Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
20eb9b4
initial file
jeremy-jung1 Jul 2, 2025
7b349b6
Uncomment global.performance
jeremy-jung1 Jul 2, 2025
07b1206
Merge branch 'W-18759774-opentelemetry-util' into W-18760780-performa…
jeremy-jung1 Jul 2, 2025
3326746
Some comments and namespacing
jeremy-jung1 Jul 7, 2025
051ea73
linting
jeremy-jung1 Jul 7, 2025
f801b5b
Some dev only flags and mods
jeremy-jung1 Jul 8, 2025
ce666ec
more info
jeremy-jung1 Jul 8, 2025
ef27e9c
More dev
jeremy-jung1 Jul 8, 2025
02cbb52
Update performance.test.js
jeremy-jung1 Jul 8, 2025
bb36629
remove some console logs
jeremy-jung1 Jul 8, 2025
1e905cf
Updates
jeremy-jung1 Jul 8, 2025
0dd809d
Update performance.test.js
jeremy-jung1 Jul 8, 2025
1e586d9
Dev mod
jeremy-jung1 Jul 8, 2025
48ff1b9
Add tests and more functionality
jeremy-jung1 Jul 9, 2025
299039c
Merge branch 'W-18759774-opentelemetry-util' into W-18760780-performa…
jeremy-jung1 Jul 9, 2025
dfb8812
Fix tests and add cleanup
jeremy-jung1 Jul 10, 2025
20b8c96
linting
jeremy-jung1 Jul 10, 2025
1d9a905
fix tests
jeremy-jung1 Jul 10, 2025
fc1f8f5
Merge branch 'W-18759774-opentelemetry-util' into W-18760780-performa…
jeremy-jung1 Jul 10, 2025
7171b8f
remove dev only changes and add more tests
jeremy-jung1 Jul 10, 2025
943a40c
Remove handling of detail being an object
jeremy-jung1 Jul 10, 2025
4072966
Some temporary? changes to get it to work
jeremy-jung1 Jul 10, 2025
6c5430d
Update config
jeremy-jung1 Jul 10, 2025
24b37e0
bundlesize increase
jeremy-jung1 Jul 11, 2025
6c301c5
Update CHANGELOG.md
jeremy-jung1 Jul 11, 2025
5a0f754
Update react-rendering.js
jeremy-jung1 Jul 11, 2025
e8dc896
Apply feedback
jeremy-jung1 Jul 14, 2025
894549b
Apply feedback
jeremy-jung1 Jul 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/pwa-kit-react-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
- 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)
- Created opentelemetry.js file with utility functions to log OTel spans and metrics [#2705] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2705)
- Update performance.js with usage of opentelemetry spans and add cleanups [#2722](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2722)

## 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)
Expand Down
18 changes: 17 additions & 1 deletion packages/pwa-kit-react-sdk/setup-jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,20 @@ jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => {
// The global performance object is available in production
// environments for both the server and the client.
// It's just the jest environment that this is not available
global.performance = performance
if (global.performance) {
// global.performance exists but is missing methods from perf_hooks in jest
if (typeof performance.mark === 'function' && !global.performance.mark) {
global.performance.mark = performance.mark.bind(performance)
}
if (typeof performance.measure === 'function' && !global.performance.measure) {
global.performance.measure = performance.measure.bind(performance)
}
if (typeof performance.clearMarks === 'function' && !global.performance.clearMarks) {
global.performance.clearMarks = performance.clearMarks.bind(performance)
}
if (typeof performance.clearMeasures === 'function' && !global.performance.clearMeasures) {
global.performance.clearMeasures = performance.clearMeasures.bind(performance)
}
} else {
global.performance = performance
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {B3Propagator} from '@opentelemetry/propagator-b3'
import {Resource} from '@opentelemetry/resources'
import {propagation} from '@opentelemetry/api'
import logger from '../../utils/logger-instance'
import {getServiceName, OTEL_CONFIG} from '../../utils/opentelemetry'
import {getServiceName, getOTELConfig} from '../../utils/opentelemetry-config'

let provider = null

Expand All @@ -27,7 +27,7 @@ export const initializeServerTracing = (options = {}) => {
const {
serviceName = options.serviceName || getServiceName(),
serviceVersion,
enabled = OTEL_CONFIG.enabled
enabled = getOTELConfig().enabled
} = options

// If tracing is disabled, return null without initializing
Expand Down
13 changes: 13 additions & 0 deletions packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
/**
* @module progressive-web-sdk/ssr/server/react-rendering
*/
import {initializeServerTracing, isServerTracingInitialized} from './opentelemetry-server'

import path from 'path'
import React from 'react'
Expand Down Expand Up @@ -122,6 +123,11 @@ export const getLocationSearch = (req, opts = {}) => {
export const render = async (req, res, next) => {
const includeServerTimingHeader = '__server_timing' in req.query
const shouldTrackPerformance = includeServerTimingHeader || process.env.SERVER_TIMING

if (!isServerTracingInitialized() && shouldTrackPerformance) {
initializeServerTracing()
}

res.__performanceTimer = new PerformanceTimer({enabled: shouldTrackPerformance})
res.__performanceTimer.mark(PERFORMANCE_MARKS.total, 'start')
const AppConfig = getAppConfig()
Expand Down Expand Up @@ -222,6 +228,11 @@ export const render = async (req, res, next) => {
// Here, we use Express's convention to invoke error middleware.
// Note, we don't have an error handling middleware yet! This is calling the
// default error handling middleware provided by Express

if (res.__performanceTimer) {
res.__performanceTimer.cleanup()
}

return next(e)
}

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

res.__performanceTimer.cleanup()

if (redirectUrl) {
res.redirect(routerContext.status || 302, redirectUrl)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,50 @@ describe('The Node SSR Environment', () => {
'max-age=0, nocache, nostore, must-revalidate'
)
}
},
{
description: `Performance timer cleanup is called during rendering`,
req: {url: '/pwa/', query: {__server_timing: '1'}},
mocks: () => {
// Mock PerformanceTimer to spy on cleanup
const PerformanceTimer = jest.requireActual('../../utils/performance').default
const originalCleanup = PerformanceTimer.prototype.cleanup
const cleanupSpy = jest.fn(originalCleanup)
PerformanceTimer.prototype.cleanup = cleanupSpy

// Store the spy for assertions
global.performanceCleanupSpy = cleanupSpy
},
assertions: (res) => {
expect(res.statusCode).toBe(200)
expect(global.performanceCleanupSpy).toHaveBeenCalled()
expect(global.performanceCleanupSpy).toHaveBeenCalledTimes(1)

// Clean up global
delete global.performanceCleanupSpy
}
},
{
description: `Performance timer cleanup is called even when rendering throws an error`,
req: {url: '/unknown-error/'}, // This URL causes an error
mocks: () => {
// Mock PerformanceTimer to spy on cleanup
const PerformanceTimer = jest.requireActual('../../utils/performance').default
const originalCleanup = PerformanceTimer.prototype.cleanup
const cleanupSpy = jest.fn(originalCleanup)
PerformanceTimer.prototype.cleanup = cleanupSpy

// Store the spy for assertions
global.performanceCleanupSpyError = cleanupSpy
},
assertions: (res) => {
expect(res.statusCode).toBe(500)
expect(global.performanceCleanupSpyError).toHaveBeenCalled()
expect(global.performanceCleanupSpyError).toHaveBeenCalledTimes(1)

// Clean up global
delete global.performanceCleanupSpyError
}
}
]

Expand Down
21 changes: 21 additions & 0 deletions packages/pwa-kit-react-sdk/src/utils/opentelemetry-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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
*/

const DEFAULT_SERVICE_NAME = 'pwa-kit-react-sdk'

// Only call this function in the server context
// This wrapper function is necessary because if the config is in the top-level code
// process will be undefined as it gets executed in the browser context and will throw an uncaught error.
export const getOTELConfig = () => {
return {
serviceName: process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE_NAME,
enabled: process.env.OTEL_SDK_ENABLED === 'true',
b3TracingEnabled: process.env.OTEL_B3_TRACING_ENABLED === 'true'
}
}

export const getServiceName = () => getOTELConfig().serviceName
146 changes: 146 additions & 0 deletions packages/pwa-kit-react-sdk/src/utils/opentelemetry-config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* 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 {getOTELConfig, getServiceName} from './opentelemetry-config'

// Mock the module to reset cache between tests
const mockModule = () => {
jest.resetModules()
return require('./opentelemetry-config')
}

describe('OpenTelemetry Config', () => {
const originalEnv = process.env

beforeEach(() => {
// Reset environment variables
process.env = {...originalEnv}

// Clear module cache to reset _cachedConfig
jest.resetModules()
})

afterEach(() => {
// Restore original environment
process.env = originalEnv
})

describe('getOTELConfig', () => {
describe('serviceName', () => {
test('should return service name when OTEL_SERVICE_NAME is set', () => {
process.env.OTEL_SERVICE_NAME = 'custom-service'

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.serviceName).toBe('custom-service')
})

test('should return default service name when OTEL_SERVICE_NAME is not set', () => {
delete process.env.OTEL_SERVICE_NAME

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.serviceName).toBe('pwa-kit-react-sdk')
})

test('should return default service name when OTEL_SERVICE_NAME is empty string', () => {
process.env.OTEL_SERVICE_NAME = ''

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.serviceName).toBe('pwa-kit-react-sdk')
})
})

describe('enabled', () => {
test('should return enabled when OTEL_SDK_ENABLED is "true"', () => {
process.env.OTEL_SDK_ENABLED = 'true'

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.enabled).toBe(true)
})

test('should return disabled when OTEL_SDK_ENABLED is not set', () => {
delete process.env.OTEL_SDK_ENABLED

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.enabled).toBe(false)
})

test('should return disabled when OTEL_SDK_ENABLED is "false"', () => {
process.env.OTEL_SDK_ENABLED = 'false'

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.enabled).toBe(false)
})

test('should return disabled when OTEL_SDK_ENABLED is any non-"true" value', () => {
const nonTrueValues = ['yes', '1', 'True', 'TRUE', 'on', 'enabled', '']

nonTrueValues.forEach((value) => {
process.env.OTEL_SDK_ENABLED = value

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.enabled).toBe(false)
})
})
})

describe('b3TracingEnabled', () => {
test('should return enabled when OTEL_B3_TRACING_ENABLED is "true"', () => {
process.env.OTEL_B3_TRACING_ENABLED = 'true'

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.b3TracingEnabled).toBe(true)
})

test('should return disabled when OTEL_B3_TRACING_ENABLED is not set', () => {
delete process.env.OTEL_B3_TRACING_ENABLED

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.b3TracingEnabled).toBe(false)
})

test('should return disabled when OTEL_B3_TRACING_ENABLED is "false"', () => {
process.env.OTEL_B3_TRACING_ENABLED = 'false'

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.b3TracingEnabled).toBe(false)
})

test('should return disabled when OTEL_B3_TRACING_ENABLED is any non-"true" value', () => {
const nonTrueValues = ['yes', '1', 'True', 'TRUE', 'on', 'enabled', '']

nonTrueValues.forEach((value) => {
process.env.OTEL_B3_TRACING_ENABLED = value

const {getOTELConfig} = mockModule()
const config = getOTELConfig()

expect(config.b3TracingEnabled).toBe(false)
})
})
})
})
})
14 changes: 3 additions & 11 deletions packages/pwa-kit-react-sdk/src/utils/opentelemetry.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,12 @@
import {trace, context, SpanStatusCode} from '@opentelemetry/api'
import {hrTimeToMilliseconds, hrTimeToTimeStamp} from '@opentelemetry/core'
import logger from './logger-instance'

const DEFAULT_SERVICE_NAME = 'pwa-kit-react-sdk'

export const OTEL_CONFIG = {
serviceName: process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE_NAME,
enabled: process.env.OTEL_SDK_ENABLED === 'true',
b3TracingEnabled: process.env.OTEL_B3_TRACING_ENABLED === 'true'
}

export const getServiceName = () => OTEL_CONFIG.serviceName
import {getOTELConfig, getServiceName} from './opentelemetry-config'

const logSpanData = (span, event = 'start', res = null) => {
const spanContext = span.spanContext()
const startTime = span.startTime

const endTime = event === 'start' ? startTime : span.endTime
const duration = event === 'start' ? 0 : hrTimeToMilliseconds(span.duration)

Expand All @@ -44,7 +36,7 @@ const logSpanData = (span, event = 'start', res = null) => {
links: [],
start_time: startTime,
end_time: endTime,
forwardTrace: OTEL_CONFIG.b3TracingEnabled
forwardTrace: getOTELConfig().b3TracingEnabled
}

// Inject B3 headers into response if available
Expand Down
7 changes: 0 additions & 7 deletions packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,6 @@ describe('OpenTelemetry Utilities', () => {
opentelemetryUtils = require('./opentelemetry')
})

describe('getServiceName', () => {
test('should return the service name from config', () => {
const serviceName = opentelemetryUtils.getServiceName()
expect(serviceName).toBe('pwa-kit-react-sdk')
})
})

describe('createSpan', () => {
test('should create a span successfully', () => {
const result = opentelemetryUtils.createSpan('test-span', {
Expand Down
Loading
Loading