diff --git a/packages/pwa-kit-react-sdk/CHANGELOG.md b/packages/pwa-kit-react-sdk/CHANGELOG.md index dea62c8181..8638af68dc 100644 --- a/packages/pwa-kit-react-sdk/CHANGELOG.md +++ b/packages/pwa-kit-react-sdk/CHANGELOG.md @@ -1,5 +1,6 @@ ## v3.11.0-dev.0 (May 23, 2025) - Fix the performance logging so that it'll capture all SSR queries, even those that result in errors [#2486](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2486) +- Created an opentelemetry server for SSR tracer intialization [#2617](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2617) ## v3.10.0 (May 22, 2025) - Fix the performance logging util to use the correct delimiter for the server-timing header. [#2225](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2295) diff --git a/packages/pwa-kit-react-sdk/package-lock.json b/packages/pwa-kit-react-sdk/package-lock.json index 1772dc7a9f..5ae23d0b10 100644 --- a/packages/pwa-kit-react-sdk/package-lock.json +++ b/packages/pwa-kit-react-sdk/package-lock.json @@ -12,6 +12,12 @@ "@loadable/babel-plugin": "^5.15.3", "@loadable/server": "^5.15.3", "@loadable/webpack-plugin": "^5.15.2", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/propagator-b3": "^1.15.1", + "@opentelemetry/resources": "^1.15.1", + "@opentelemetry/sdk-trace-base": "^1.15.1", + "@opentelemetry/sdk-trace-node": "^1.15.1", + "@opentelemetry/semantic-conventions": "^1.15.1", "@tanstack/react-query": "^4.28.0", "cross-env": "^5.2.1", "event-emitter": "^0.3.5", @@ -637,6 +643,173 @@ "webpack": ">=4.6.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", + "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.30.1.tgz", + "integrity": "sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.1.tgz", + "integrity": "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", + "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", + "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.1.tgz", + "integrity": "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "1.30.1", + "@opentelemetry/core": "1.30.1", + "@opentelemetry/propagator-b3": "1.30.1", + "@opentelemetry/propagator-jaeger": "1.30.1", + "@opentelemetry/sdk-trace-base": "1.30.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.34.0.tgz", + "integrity": "sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", diff --git a/packages/pwa-kit-react-sdk/package.json b/packages/pwa-kit-react-sdk/package.json index 4ec7b7deb0..d952ff2c49 100644 --- a/packages/pwa-kit-react-sdk/package.json +++ b/packages/pwa-kit-react-sdk/package.json @@ -37,6 +37,12 @@ "@loadable/babel-plugin": "^5.15.3", "@loadable/server": "^5.15.3", "@loadable/webpack-plugin": "^5.15.2", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/propagator-b3": "^1.15.1", + "@opentelemetry/resources": "^1.15.1", + "@opentelemetry/sdk-trace-base": "^1.15.1", + "@opentelemetry/sdk-trace-node": "^1.15.1", + "@opentelemetry/semantic-conventions": "^1.15.1", "@salesforce/pwa-kit-runtime": "3.11.0-dev.0", "@tanstack/react-query": "^4.28.0", "cross-env": "^5.2.1", diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js new file mode 100644 index 0000000000..1d6523ae7c --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node' +import {SimpleSpanProcessor} from '@opentelemetry/sdk-trace-base' +import {B3Propagator} from '@opentelemetry/propagator-b3' +import {Resource} from '@opentelemetry/resources' +import {propagation} from '@opentelemetry/api' +import logger from '../../utils/logger-instance' + +const DEFAULT_SERVICE_NAME = 'pwa-kit-react-sdk' + +let provider = null + +/** + * Initialize OpenTelemetry tracing for server-side rendering + * @param {Object} options + * @param {string} [options.serviceName] + * @param {string} [options.serviceVersion] + * @param {boolean} [options.enabled] + * @returns {NodeTracerProvider|null} + */ +export const initializeServerTracing = (options = {}) => { + const { + serviceName = process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE_NAME, + serviceVersion, + enabled = process.env.OTEL_SDK_ENABLED === 'true' + } = options + + // If tracing is disabled, return null without initializing + if (!enabled) { + return null + } + + try { + // Build resource attributes + const resourceAttributes = { + 'service.name': serviceName + } + + // Add service version if provided + if (serviceVersion) { + resourceAttributes['service.version'] = serviceVersion + } + + // Initialize the tracer provider + provider = new NodeTracerProvider({ + resource: new Resource(resourceAttributes), + spanProcessor: new SimpleSpanProcessor() + }) + + // Add B3 propagator + propagation.setGlobalPropagator(new B3Propagator()) + provider.register() + + return provider + } catch (error) { + // Log errors from OpenTelemetry initialization + logger.error('Failed to initialize OpenTelemetry provider', { + namespace: 'opentelemetry-server.initializeServerTracing', + additionalProperties: {error: error.message} + }) + return null + } +} + +/** + * Shutdown OpenTelemetry tracing and clean up resources + * @returns {Promise} + */ +export const shutdownServerTracing = async () => { + if (provider) { + try { + await provider.shutdown() + provider = null // Clean up after successful shutdown + } catch (error) { + logger.warn('Failed to shutdown OpenTelemetry provider', { + namespace: 'opentelemetry-server.shutdownServerTracing', + additionalProperties: {error: error.message} + }) + } + } +} + +/** + * Get the current OpenTelemetry provider instance + * @returns {NodeTracerProvider|null} The current provider or null if not initialized + */ +export const getServerTracingProvider = () => { + return provider +} + +/** + * Check if OpenTelemetry tracing is currently initialized + * @returns {boolean} True if tracing is initialized, false otherwise + */ +export const isServerTracingInitialized = () => { + return provider !== null +} diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.test.js b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.test.js new file mode 100644 index 0000000000..93c452965b --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.test.js @@ -0,0 +1,453 @@ +/** + * @jest-environment node + */ +/* + * Copyright (c) 2025, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +// The @jest-environment comment block *MUST* be the first line of the file for the tests to pass. +// That conflicts with the monorepo header rule, so we must disable the rule! +/* eslint-disable header/header */ + +// Mock OpenTelemetry dependencies +jest.mock('@opentelemetry/sdk-trace-node', () => ({ + NodeTracerProvider: jest.fn() +})) + +jest.mock('@opentelemetry/sdk-trace-base', () => ({ + SimpleSpanProcessor: jest.fn() +})) + +jest.mock('@opentelemetry/propagator-b3', () => ({ + B3Propagator: jest.fn() +})) + +jest.mock('@opentelemetry/resources', () => ({ + Resource: jest.fn() +})) + +jest.mock('@opentelemetry/api', () => ({ + propagation: { + setGlobalPropagator: jest.fn() + } +})) + +jest.mock('../../utils/logger-instance', () => ({ + warn: jest.fn(), + error: jest.fn(), + info: jest.fn(), + debug: jest.fn() +})) + +describe('OpenTelemetry Server Tracing', () => { + let mockNodeTracerProvider + let mockSimpleSpanProcessor + let mockB3Propagator + let mockResource + let mockPropagation + let mockLogger + let initializeServerTracing + let shutdownServerTracing + let isServerTracingInitialized + let defaultOptions + + beforeEach(() => { + // Reset all mocks + jest.clearAllMocks() + + // Get mocked constructors + /* eslint-disable @typescript-eslint/no-var-requires */ + const {NodeTracerProvider} = require('@opentelemetry/sdk-trace-node') + const {SimpleSpanProcessor} = require('@opentelemetry/sdk-trace-base') + const {B3Propagator} = require('@opentelemetry/propagator-b3') + const {Resource} = require('@opentelemetry/resources') + const {propagation} = require('@opentelemetry/api') + const logger = require('../../utils/logger-instance') + const opentelemetryServer = require('./opentelemetry-server') + /* eslint-enable @typescript-eslint/no-var-requires */ + + mockNodeTracerProvider = NodeTracerProvider + mockSimpleSpanProcessor = SimpleSpanProcessor + mockB3Propagator = B3Propagator + mockResource = Resource + mockPropagation = propagation + mockLogger = logger + defaultOptions = {enabled: true} + + // Set up mock instances + const mockProviderInstance = { + register: jest.fn(), + shutdown: jest.fn() + } + + const mockResourceInstance = {} + const mockSpanProcessorInstance = {} + const mockB3PropagatorInstance = {} + + // Configure mocks + mockNodeTracerProvider.mockImplementation(() => mockProviderInstance) + mockResource.mockImplementation(() => mockResourceInstance) + mockSimpleSpanProcessor.mockImplementation(() => mockSpanProcessorInstance) + mockB3Propagator.mockImplementation(() => mockB3PropagatorInstance) + + // Import the functions after mocks are set up + initializeServerTracing = opentelemetryServer.initializeServerTracing + shutdownServerTracing = opentelemetryServer.shutdownServerTracing + isServerTracingInitialized = opentelemetryServer.isServerTracingInitialized + }) + + afterEach(async () => { + // Clean up any existing provider + if (shutdownServerTracing) { + await shutdownServerTracing() + } + + // Reset module state to ensure clean state between tests + jest.resetModules() + }) + + describe('initializeServerTracing', () => { + test('should successfully initialize OpenTelemetry tracing with default options', () => { + const result = initializeServerTracing(defaultOptions) + + // Verify NodeTracerProvider was called with correct resource and span processor + expect(mockNodeTracerProvider).toHaveBeenCalledWith({ + resource: expect.any(Object), + spanProcessor: expect.any(Object) + }) + + // Verify Resource was created with correct service name only (no version by default) + expect(mockResource).toHaveBeenCalledWith({ + 'service.name': 'pwa-kit-react-sdk' + }) + + // Verify span processor was created + expect(mockSimpleSpanProcessor).toHaveBeenCalled() + + // Verify B3 propagator was set globally + expect(mockB3Propagator).toHaveBeenCalled() + expect(mockPropagation.setGlobalPropagator).toHaveBeenCalledWith(expect.any(Object)) + + // Verify provider was registered + expect(result.register).toHaveBeenCalled() + + // Verify the provider was returned + expect(result).toBeDefined() + + // Verify initialization state + expect(isServerTracingInitialized()).toBe(true) + }) + + test('should initialize with custom service name', () => { + const customServiceName = 'my-custom-service' + const result = initializeServerTracing({ + ...defaultOptions, + serviceName: customServiceName + }) + + // Verify Resource was created with custom service name only + expect(mockResource).toHaveBeenCalledWith({ + 'service.name': customServiceName + }) + + expect(result).toBeDefined() + }) + + test('should initialize with service version', () => { + const serviceVersion = '1.2.3' + const result = initializeServerTracing({...defaultOptions, serviceVersion}) + + // Verify Resource was created with service version + expect(mockResource).toHaveBeenCalledWith({ + 'service.name': 'pwa-kit-react-sdk', + 'service.version': serviceVersion + }) + + expect(result).toBeDefined() + }) + + test('should initialize with both service name and version', () => { + const customServiceName = 'my-service' + const serviceVersion = '2.0.0' + const result = initializeServerTracing({ + ...defaultOptions, + serviceName: customServiceName, + serviceVersion + }) + + // Verify Resource was created with both attributes + expect(mockResource).toHaveBeenCalledWith({ + 'service.name': customServiceName, + 'service.version': serviceVersion + }) + + expect(result).toBeDefined() + }) + + test('should return null when tracing is disabled', () => { + const result = initializeServerTracing({enabled: false}) + + // Verify no provider was created + expect(mockNodeTracerProvider).not.toHaveBeenCalled() + expect(mockResource).not.toHaveBeenCalled() + expect(result).toBeNull() + expect(isServerTracingInitialized()).toBe(false) + }) + + test('should handle initialization errors gracefully', () => { + // Mock NodeTracerProvider to throw an error + mockNodeTracerProvider.mockImplementation(() => { + throw new Error('OpenTelemetry initialization failed') + }) + + const result = initializeServerTracing(defaultOptions) + + // Verify error was logged + expect(mockLogger.error).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider', + { + namespace: 'opentelemetry-server.initializeServerTracing', + additionalProperties: {error: 'OpenTelemetry initialization failed'} + } + ) + + // Verify null was returned + expect(result).toBeNull() + + // Verify initialization state + expect(isServerTracingInitialized()).toBe(false) + }) + + test('should handle resource creation errors', () => { + // Mock Resource to throw an error + mockResource.mockImplementation(() => { + throw new Error('Resource creation failed') + }) + + const result = initializeServerTracing(defaultOptions) + + // Verify error was logged + expect(mockLogger.error).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider', + { + namespace: 'opentelemetry-server.initializeServerTracing', + additionalProperties: {error: 'Resource creation failed'} + } + ) + + // Verify null was returned + expect(result).toBeNull() + }) + + test('should handle provider registration errors', () => { + // Set up mock provider that throws on register + const mockProviderInstance = { + register: jest.fn().mockImplementation(() => { + throw new Error('Provider registration failed') + }), + shutdown: jest.fn() + } + mockNodeTracerProvider.mockImplementation(() => mockProviderInstance) + + const result = initializeServerTracing(defaultOptions) + + // Verify error was logged + expect(mockLogger.error).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider', + { + namespace: 'opentelemetry-server.initializeServerTracing', + additionalProperties: {error: 'Provider registration failed'} + } + ) + + // Verify null was returned + expect(result).toBeNull() + }) + + describe('environment variable handling', () => { + const originalEnv = process.env + + beforeEach(() => { + jest.resetModules() + process.env = {...originalEnv} + }) + + afterEach(() => { + process.env = originalEnv + }) + + test('should use OTEL_SERVICE_NAME environment variable when provided', () => { + // Clear previous mock calls + mockResource.mockClear() + mockNodeTracerProvider.mockClear() + + process.env.OTEL_SERVICE_NAME = 'env-service-name' + + const result = initializeServerTracing({enabled: true}) + + expect(mockResource).toHaveBeenCalledWith({ + 'service.name': 'env-service-name' + }) + expect(result).toBeDefined() + }) + + test('should enable tracing when OTEL_SDK_ENABLED is true', () => { + // Clear previous mock calls + mockResource.mockClear() + mockNodeTracerProvider.mockClear() + + process.env.OTEL_SDK_ENABLED = 'true' + + const result = initializeServerTracing() + + expect(mockNodeTracerProvider).toHaveBeenCalled() + expect(result).toBeDefined() + }) + + test('should disable tracing when OTEL_SDK_ENABLED is false', () => { + process.env.OTEL_SDK_ENABLED = 'false' + + // Re-import to get fresh module with updated env + // eslint-disable-next-line @typescript-eslint/no-var-requires + const opentelemetryServer = require('./opentelemetry-server') + const {initializeServerTracing} = opentelemetryServer + + const result = initializeServerTracing(defaultOptions) + + expect(mockNodeTracerProvider).not.toHaveBeenCalled() + expect(result).toBeNull() + }) + + test('should disable tracing when OTEL_SDK_ENABLED is not set', () => { + delete process.env.OTEL_SDK_ENABLED + + // Re-import to get fresh module with updated env + // eslint-disable-next-line @typescript-eslint/no-var-requires + const opentelemetryServer = require('./opentelemetry-server') + const {initializeServerTracing} = opentelemetryServer + + const result = initializeServerTracing(defaultOptions) + + expect(mockNodeTracerProvider).not.toHaveBeenCalled() + expect(result).toBeNull() + }) + }) + }) + + describe('shutdownServerTracing', () => { + test('should successfully shutdown OpenTelemetry provider when provider exists', async () => { + // First initialize to create a provider + const provider = initializeServerTracing(defaultOptions) + expect(provider).toBeDefined() + + // Then shutdown + await shutdownServerTracing() + + // Verify shutdown was called + expect(provider.shutdown).toHaveBeenCalled() + + // Verify provider is cleaned up + expect(isServerTracingInitialized()).toBe(false) + }) + + test('should handle shutdown when no provider exists', async () => { + // Don't initialize first, so no provider exists + expect(isServerTracingInitialized()).toBe(false) + + await shutdownServerTracing() + + // Verify no error was logged (graceful handling) + expect(mockLogger.warn).not.toHaveBeenCalled() + }) + + test('should handle shutdown errors gracefully', async () => { + // Set up mock provider that throws on shutdown + const mockProviderInstance = { + register: jest.fn(), + shutdown: jest.fn().mockRejectedValue(new Error('Shutdown failed')) + } + mockNodeTracerProvider.mockImplementation(() => mockProviderInstance) + + // Initialize to create provider + const provider = initializeServerTracing(defaultOptions) + expect(provider).toBeDefined() + + // Shutdown should not throw + await expect(shutdownServerTracing()).resolves.not.toThrow() + + // Verify error was logged + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Failed to shutdown OpenTelemetry provider', + { + namespace: 'opentelemetry-server.shutdownServerTracing', + additionalProperties: {error: 'Shutdown failed'} + } + ) + }) + }) + + describe('isServerTracingInitialized', () => { + test('should return true when provider is initialized', () => { + initializeServerTracing(defaultOptions) + expect(isServerTracingInitialized()).toBe(true) + }) + + test('should return false when not initialized', () => { + expect(isServerTracingInitialized()).toBe(false) + }) + + test('should return false after shutdown', async () => { + initializeServerTracing(defaultOptions) + expect(isServerTracingInitialized()).toBe(true) + + await shutdownServerTracing() + expect(isServerTracingInitialized()).toBe(false) + }) + }) + + describe('integration scenarios', () => { + test('should handle complete lifecycle: initialize -> shutdown', async () => { + // Initialize + const provider = initializeServerTracing(defaultOptions) + expect(provider).toBeDefined() + expect(isServerTracingInitialized()).toBe(true) + + // Verify all setup was done + expect(mockNodeTracerProvider).toHaveBeenCalled() + expect(mockResource).toHaveBeenCalled() + expect(mockSimpleSpanProcessor).toHaveBeenCalled() + expect(mockB3Propagator).toHaveBeenCalled() + expect(mockPropagation.setGlobalPropagator).toHaveBeenCalled() + expect(provider.register).toHaveBeenCalled() + + // Shutdown + await shutdownServerTracing() + expect(provider.shutdown).toHaveBeenCalled() + expect(isServerTracingInitialized()).toBe(false) + }) + + test('should handle failed initialization followed by shutdown', async () => { + // Mock initialization to fail + mockNodeTracerProvider.mockImplementation(() => { + throw new Error('Initialization failed') + }) + + // Initialize should fail + const provider = initializeServerTracing(defaultOptions) + expect(provider).toBeNull() + expect(isServerTracingInitialized()).toBe(false) + expect(mockLogger.error).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider', + { + namespace: 'opentelemetry-server.initializeServerTracing', + additionalProperties: {error: 'Initialization failed'} + } + ) + + // Shutdown should still work gracefully + await expect(shutdownServerTracing()).resolves.not.toThrow() + }) + }) +})