Skip to content

Commit 0d75e65

Browse files
authored
Merge pull request #2722 from SalesforceCommerceCloud/W-18760780-performancejs
@W-18760780 Integrate opentelemetry into performance.js
2 parents 4fa34a2 + 894549b commit 0d75e65

File tree

13 files changed

+854
-75
lines changed

13 files changed

+854
-75
lines changed

packages/pwa-kit-react-sdk/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
- 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)
33
- Created an opentelemetry server for SSR tracer intialization [#2617](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2617)
44
- Created opentelemetry.js file with utility functions to log OTel spans and metrics [#2705] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2705)
5+
- Update performance.js with usage of opentelemetry spans and add cleanups [#2722](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2722)
56

67
## v3.10.0 (May 22, 2025)
78
- Fix the performance logging util to use the correct delimiter for the server-timing header. [#2225](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2295)

packages/pwa-kit-react-sdk/setup-jest.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,20 @@ jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => {
4040
// The global performance object is available in production
4141
// environments for both the server and the client.
4242
// It's just the jest environment that this is not available
43-
global.performance = performance
43+
if (global.performance) {
44+
// global.performance exists but is missing methods from perf_hooks in jest
45+
if (typeof performance.mark === 'function' && !global.performance.mark) {
46+
global.performance.mark = performance.mark.bind(performance)
47+
}
48+
if (typeof performance.measure === 'function' && !global.performance.measure) {
49+
global.performance.measure = performance.measure.bind(performance)
50+
}
51+
if (typeof performance.clearMarks === 'function' && !global.performance.clearMarks) {
52+
global.performance.clearMarks = performance.clearMarks.bind(performance)
53+
}
54+
if (typeof performance.clearMeasures === 'function' && !global.performance.clearMeasures) {
55+
global.performance.clearMeasures = performance.clearMeasures.bind(performance)
56+
}
57+
} else {
58+
global.performance = performance
59+
}

packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {B3Propagator} from '@opentelemetry/propagator-b3'
1111
import {Resource} from '@opentelemetry/resources'
1212
import {propagation} from '@opentelemetry/api'
1313
import logger from '../../utils/logger-instance'
14-
import {getServiceName, OTEL_CONFIG} from '../../utils/opentelemetry'
14+
import {getServiceName, getOTELConfig} from '../../utils/opentelemetry-config'
1515

1616
let provider = null
1717

@@ -27,7 +27,7 @@ export const initializeServerTracing = (options = {}) => {
2727
const {
2828
serviceName = options.serviceName || getServiceName(),
2929
serviceVersion,
30-
enabled = OTEL_CONFIG.enabled
30+
enabled = getOTELConfig().enabled
3131
} = options
3232

3333
// If tracing is disabled, return null without initializing

packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
/**
99
* @module progressive-web-sdk/ssr/server/react-rendering
1010
*/
11+
import {initializeServerTracing, isServerTracingInitialized} from './opentelemetry-server'
1112

1213
import path from 'path'
1314
import React from 'react'
@@ -122,6 +123,11 @@ export const getLocationSearch = (req, opts = {}) => {
122123
export const render = async (req, res, next) => {
123124
const includeServerTimingHeader = '__server_timing' in req.query
124125
const shouldTrackPerformance = includeServerTimingHeader || process.env.SERVER_TIMING
126+
127+
if (!isServerTracingInitialized() && shouldTrackPerformance) {
128+
initializeServerTracing()
129+
}
130+
125131
res.__performanceTimer = new PerformanceTimer({enabled: shouldTrackPerformance})
126132
res.__performanceTimer.mark(PERFORMANCE_MARKS.total, 'start')
127133
const AppConfig = getAppConfig()
@@ -222,6 +228,11 @@ export const render = async (req, res, next) => {
222228
// Here, we use Express's convention to invoke error middleware.
223229
// Note, we don't have an error handling middleware yet! This is calling the
224230
// default error handling middleware provided by Express
231+
232+
if (res.__performanceTimer) {
233+
res.__performanceTimer.cleanup()
234+
}
235+
225236
return next(e)
226237
}
227238

@@ -244,6 +255,8 @@ export const render = async (req, res, next) => {
244255
res.set('Cache-Control', NO_CACHE)
245256
}
246257

258+
res.__performanceTimer.cleanup()
259+
247260
if (redirectUrl) {
248261
res.redirect(routerContext.status || 302, redirectUrl)
249262
} else {

packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,50 @@ describe('The Node SSR Environment', () => {
739739
'max-age=0, nocache, nostore, must-revalidate'
740740
)
741741
}
742+
},
743+
{
744+
description: `Performance timer cleanup is called during rendering`,
745+
req: {url: '/pwa/', query: {__server_timing: '1'}},
746+
mocks: () => {
747+
// Mock PerformanceTimer to spy on cleanup
748+
const PerformanceTimer = jest.requireActual('../../utils/performance').default
749+
const originalCleanup = PerformanceTimer.prototype.cleanup
750+
const cleanupSpy = jest.fn(originalCleanup)
751+
PerformanceTimer.prototype.cleanup = cleanupSpy
752+
753+
// Store the spy for assertions
754+
global.performanceCleanupSpy = cleanupSpy
755+
},
756+
assertions: (res) => {
757+
expect(res.statusCode).toBe(200)
758+
expect(global.performanceCleanupSpy).toHaveBeenCalled()
759+
expect(global.performanceCleanupSpy).toHaveBeenCalledTimes(1)
760+
761+
// Clean up global
762+
delete global.performanceCleanupSpy
763+
}
764+
},
765+
{
766+
description: `Performance timer cleanup is called even when rendering throws an error`,
767+
req: {url: '/unknown-error/'}, // This URL causes an error
768+
mocks: () => {
769+
// Mock PerformanceTimer to spy on cleanup
770+
const PerformanceTimer = jest.requireActual('../../utils/performance').default
771+
const originalCleanup = PerformanceTimer.prototype.cleanup
772+
const cleanupSpy = jest.fn(originalCleanup)
773+
PerformanceTimer.prototype.cleanup = cleanupSpy
774+
775+
// Store the spy for assertions
776+
global.performanceCleanupSpyError = cleanupSpy
777+
},
778+
assertions: (res) => {
779+
expect(res.statusCode).toBe(500)
780+
expect(global.performanceCleanupSpyError).toHaveBeenCalled()
781+
expect(global.performanceCleanupSpyError).toHaveBeenCalledTimes(1)
782+
783+
// Clean up global
784+
delete global.performanceCleanupSpyError
785+
}
742786
}
743787
]
744788

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
const DEFAULT_SERVICE_NAME = 'pwa-kit-react-sdk'
9+
10+
// Only call this function in the server context
11+
// This wrapper function is necessary because if the config is in the top-level code
12+
// process will be undefined as it gets executed in the browser context and will throw an uncaught error.
13+
export const getOTELConfig = () => {
14+
return {
15+
serviceName: process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE_NAME,
16+
enabled: process.env.OTEL_SDK_ENABLED === 'true',
17+
b3TracingEnabled: process.env.OTEL_B3_TRACING_ENABLED === 'true'
18+
}
19+
}
20+
21+
export const getServiceName = () => getOTELConfig().serviceName
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import {getOTELConfig, getServiceName} from './opentelemetry-config'
9+
10+
// Mock the module to reset cache between tests
11+
const mockModule = () => {
12+
jest.resetModules()
13+
return require('./opentelemetry-config')
14+
}
15+
16+
describe('OpenTelemetry Config', () => {
17+
const originalEnv = process.env
18+
19+
beforeEach(() => {
20+
// Reset environment variables
21+
process.env = {...originalEnv}
22+
23+
// Clear module cache to reset _cachedConfig
24+
jest.resetModules()
25+
})
26+
27+
afterEach(() => {
28+
// Restore original environment
29+
process.env = originalEnv
30+
})
31+
32+
describe('getOTELConfig', () => {
33+
describe('serviceName', () => {
34+
test('should return service name when OTEL_SERVICE_NAME is set', () => {
35+
process.env.OTEL_SERVICE_NAME = 'custom-service'
36+
37+
const {getOTELConfig} = mockModule()
38+
const config = getOTELConfig()
39+
40+
expect(config.serviceName).toBe('custom-service')
41+
})
42+
43+
test('should return default service name when OTEL_SERVICE_NAME is not set', () => {
44+
delete process.env.OTEL_SERVICE_NAME
45+
46+
const {getOTELConfig} = mockModule()
47+
const config = getOTELConfig()
48+
49+
expect(config.serviceName).toBe('pwa-kit-react-sdk')
50+
})
51+
52+
test('should return default service name when OTEL_SERVICE_NAME is empty string', () => {
53+
process.env.OTEL_SERVICE_NAME = ''
54+
55+
const {getOTELConfig} = mockModule()
56+
const config = getOTELConfig()
57+
58+
expect(config.serviceName).toBe('pwa-kit-react-sdk')
59+
})
60+
})
61+
62+
describe('enabled', () => {
63+
test('should return enabled when OTEL_SDK_ENABLED is "true"', () => {
64+
process.env.OTEL_SDK_ENABLED = 'true'
65+
66+
const {getOTELConfig} = mockModule()
67+
const config = getOTELConfig()
68+
69+
expect(config.enabled).toBe(true)
70+
})
71+
72+
test('should return disabled when OTEL_SDK_ENABLED is not set', () => {
73+
delete process.env.OTEL_SDK_ENABLED
74+
75+
const {getOTELConfig} = mockModule()
76+
const config = getOTELConfig()
77+
78+
expect(config.enabled).toBe(false)
79+
})
80+
81+
test('should return disabled when OTEL_SDK_ENABLED is "false"', () => {
82+
process.env.OTEL_SDK_ENABLED = 'false'
83+
84+
const {getOTELConfig} = mockModule()
85+
const config = getOTELConfig()
86+
87+
expect(config.enabled).toBe(false)
88+
})
89+
90+
test('should return disabled when OTEL_SDK_ENABLED is any non-"true" value', () => {
91+
const nonTrueValues = ['yes', '1', 'True', 'TRUE', 'on', 'enabled', '']
92+
93+
nonTrueValues.forEach((value) => {
94+
process.env.OTEL_SDK_ENABLED = value
95+
96+
const {getOTELConfig} = mockModule()
97+
const config = getOTELConfig()
98+
99+
expect(config.enabled).toBe(false)
100+
})
101+
})
102+
})
103+
104+
describe('b3TracingEnabled', () => {
105+
test('should return enabled when OTEL_B3_TRACING_ENABLED is "true"', () => {
106+
process.env.OTEL_B3_TRACING_ENABLED = 'true'
107+
108+
const {getOTELConfig} = mockModule()
109+
const config = getOTELConfig()
110+
111+
expect(config.b3TracingEnabled).toBe(true)
112+
})
113+
114+
test('should return disabled when OTEL_B3_TRACING_ENABLED is not set', () => {
115+
delete process.env.OTEL_B3_TRACING_ENABLED
116+
117+
const {getOTELConfig} = mockModule()
118+
const config = getOTELConfig()
119+
120+
expect(config.b3TracingEnabled).toBe(false)
121+
})
122+
123+
test('should return disabled when OTEL_B3_TRACING_ENABLED is "false"', () => {
124+
process.env.OTEL_B3_TRACING_ENABLED = 'false'
125+
126+
const {getOTELConfig} = mockModule()
127+
const config = getOTELConfig()
128+
129+
expect(config.b3TracingEnabled).toBe(false)
130+
})
131+
132+
test('should return disabled when OTEL_B3_TRACING_ENABLED is any non-"true" value', () => {
133+
const nonTrueValues = ['yes', '1', 'True', 'TRUE', 'on', 'enabled', '']
134+
135+
nonTrueValues.forEach((value) => {
136+
process.env.OTEL_B3_TRACING_ENABLED = value
137+
138+
const {getOTELConfig} = mockModule()
139+
const config = getOTELConfig()
140+
141+
expect(config.b3TracingEnabled).toBe(false)
142+
})
143+
})
144+
})
145+
})
146+
})

packages/pwa-kit-react-sdk/src/utils/opentelemetry.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,12 @@
88
import {trace, context, SpanStatusCode} from '@opentelemetry/api'
99
import {hrTimeToMilliseconds, hrTimeToTimeStamp} from '@opentelemetry/core'
1010
import logger from './logger-instance'
11-
12-
const DEFAULT_SERVICE_NAME = 'pwa-kit-react-sdk'
13-
14-
export const OTEL_CONFIG = {
15-
serviceName: process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE_NAME,
16-
enabled: process.env.OTEL_SDK_ENABLED === 'true',
17-
b3TracingEnabled: process.env.OTEL_B3_TRACING_ENABLED === 'true'
18-
}
19-
20-
export const getServiceName = () => OTEL_CONFIG.serviceName
11+
import {getOTELConfig, getServiceName} from './opentelemetry-config'
2112

2213
const logSpanData = (span, event = 'start', res = null) => {
2314
const spanContext = span.spanContext()
2415
const startTime = span.startTime
16+
2517
const endTime = event === 'start' ? startTime : span.endTime
2618
const duration = event === 'start' ? 0 : hrTimeToMilliseconds(span.duration)
2719

@@ -44,7 +36,7 @@ const logSpanData = (span, event = 'start', res = null) => {
4436
links: [],
4537
start_time: startTime,
4638
end_time: endTime,
47-
forwardTrace: OTEL_CONFIG.b3TracingEnabled
39+
forwardTrace: getOTELConfig().b3TracingEnabled
4840
}
4941

5042
// Inject B3 headers into response if available

packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,6 @@ describe('OpenTelemetry Utilities', () => {
103103
opentelemetryUtils = require('./opentelemetry')
104104
})
105105

106-
describe('getServiceName', () => {
107-
test('should return the service name from config', () => {
108-
const serviceName = opentelemetryUtils.getServiceName()
109-
expect(serviceName).toBe('pwa-kit-react-sdk')
110-
})
111-
})
112-
113106
describe('createSpan', () => {
114107
test('should create a span successfully', () => {
115108
const result = opentelemetryUtils.createSpan('test-span', {

0 commit comments

Comments
 (0)