From ae56f7950da4cae615418fcb6dd55b6f466584a2 Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Fri, 20 Jun 2025 16:04:24 -0700 Subject: [PATCH 001/626] Creating tests and a server for open telemetry within the pwa-kit-react-sdk folder acting as a service provider --- packages/pwa-kit-react-sdk/package.json | 7 + .../src/ssr/server/opentelemetry-server.js | 74 +++++ .../ssr/server/opentelemetry-server.test.js | 305 ++++++++++++++++++ 3 files changed, 386 insertions(+) create mode 100644 packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js create mode 100644 packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.test.js diff --git a/packages/pwa-kit-react-sdk/package.json b/packages/pwa-kit-react-sdk/package.json index 4ec7b7deb0..b01d4b46db 100644 --- a/packages/pwa-kit-react-sdk/package.json +++ b/packages/pwa-kit-react-sdk/package.json @@ -37,6 +37,13 @@ "@loadable/babel-plugin": "^5.15.3", "@loadable/server": "^5.15.3", "@loadable/webpack-plugin": "^5.15.2", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.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..5eecfe5f74 --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024, 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 {SemanticResourceAttributes} from '@opentelemetry/semantic-conventions' +import {propagation} from '@opentelemetry/api' + +const SERVICE_NAME = 'pwa-kit-react-sdk' + +let provider = null + +/** + * Initialize OpenTelemetry tracing for server-side rendering + * @returns {NodeTracerProvider|null} The initialized provider or null if initialization failed + */ +export const initializeServerTracing = () => { + try { + // Initialize the tracer provider + provider = new NodeTracerProvider({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME + }) + }) + + // Add B3 propagator + provider.addSpanProcessor(new SimpleSpanProcessor()) + propagation.setGlobalPropagator(new B3Propagator()) + provider.register() + + return provider + } catch (error) { + // Log errors from OpenTelemetry initialization + console.warn('Failed to initialize OpenTelemetry provider:', 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) { + console.warn('Failed to shutdown OpenTelemetry provider:', 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..6e5274d964 --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.test.js @@ -0,0 +1,305 @@ +/** + * @jest-environment node + */ +/* + * Copyright (c) 2024, 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 */ + +import { + initializeServerTracing, + shutdownServerTracing, + isServerTracingInitialized +} from './opentelemetry-server' + +// 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/semantic-conventions', () => ({ + SemanticResourceAttributes: { + SERVICE_NAME: 'service.name' + } +})) + +jest.mock('@opentelemetry/api', () => ({ + propagation: { + setGlobalPropagator: jest.fn() + } +})) + +describe('OpenTelemetry Server Tracing', () => { + let mockNodeTracerProvider + let mockSimpleSpanProcessor + let mockB3Propagator + let mockResource + let mockPropagation + let consoleWarnSpy + + beforeEach(() => { + // Reset all mocks + jest.clearAllMocks() + + // Set up console.warn spy + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}) + + // 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') + /* eslint-enable @typescript-eslint/no-var-requires */ + + mockNodeTracerProvider = NodeTracerProvider + mockSimpleSpanProcessor = SimpleSpanProcessor + mockB3Propagator = B3Propagator + mockResource = Resource + mockPropagation = propagation + + // Set up mock instances + const mockProviderInstance = { + addSpanProcessor: jest.fn(), + 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) + }) + + afterEach(() => { + consoleWarnSpy.mockRestore() + }) + + describe('initializeServerTracing', () => { + test('should successfully initialize OpenTelemetry tracing', () => { + const result = initializeServerTracing() + + // Verify NodeTracerProvider was called with correct resource + expect(mockNodeTracerProvider).toHaveBeenCalledWith({ + resource: expect.any(Object) + }) + + // Verify Resource was created with correct service name + expect(mockResource).toHaveBeenCalledWith({ + 'service.name': 'pwa-kit-react-sdk' + }) + + // Verify span processor was added + expect(mockSimpleSpanProcessor).toHaveBeenCalled() + expect(result.addSpanProcessor).toHaveBeenCalledWith(expect.any(Object)) + + // 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 handle initialization errors gracefully', () => { + // Mock NodeTracerProvider to throw an error + mockNodeTracerProvider.mockImplementation(() => { + throw new Error('OpenTelemetry initialization failed') + }) + + const result = initializeServerTracing() + + // Verify error was logged + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider:', + '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() + + // Verify error was logged + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider:', + '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 = { + addSpanProcessor: jest.fn(), + register: jest.fn().mockImplementation(() => { + throw new Error('Provider registration failed') + }), + shutdown: jest.fn() + } + mockNodeTracerProvider.mockImplementation(() => mockProviderInstance) + + const result = initializeServerTracing() + + // Verify error was logged + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider:', + 'Provider registration failed' + ) + + // Verify null was returned + expect(result).toBeNull() + }) + }) + + describe('shutdownServerTracing', () => { + test('should successfully shutdown OpenTelemetry provider when provider exists', async () => { + // First initialize to create a provider + const provider = initializeServerTracing() + 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(consoleWarnSpy).not.toHaveBeenCalled() + }) + + test('should handle shutdown errors gracefully', async () => { + // Set up mock provider that throws on shutdown + const mockProviderInstance = { + addSpanProcessor: jest.fn(), + register: jest.fn(), + shutdown: jest.fn().mockRejectedValue(new Error('Shutdown failed')) + } + mockNodeTracerProvider.mockImplementation(() => mockProviderInstance) + + // Initialize to create provider + const provider = initializeServerTracing() + expect(provider).toBeDefined() + + // Shutdown should not throw + await expect(shutdownServerTracing()).resolves.not.toThrow() + + // Verify error was logged + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Failed to shutdown OpenTelemetry provider:', + 'Shutdown failed' + ) + }) + }) + + describe('isServerTracingInitialized', () => { + test('should return true when provider is initialized', () => { + initializeServerTracing() + expect(isServerTracingInitialized()).toBe(true) + }) + + test('should return false when not initialized', () => { + expect(isServerTracingInitialized()).toBe(false) + }) + + test('should return false after shutdown', async () => { + initializeServerTracing() + 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() + 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() + expect(provider).toBeNull() + expect(isServerTracingInitialized()).toBe(false) + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider:', + 'Initialization failed' + ) + + // Shutdown should still work gracefully + await expect(shutdownServerTracing()).resolves.not.toThrow() + }) + }) +}) From 6f50178f24b636194e64c809208fb56fe06a80c2 Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Mon, 23 Jun 2025 13:10:46 -0700 Subject: [PATCH 002/626] Updating my package lock.json file for the pwa-kit-react-sdk folder with open telemetry dependencies --- packages/pwa-kit-react-sdk/package-lock.json | 174 +++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/packages/pwa-kit-react-sdk/package-lock.json b/packages/pwa-kit-react-sdk/package-lock.json index 1772dc7a9f..dbdbce174c 100644 --- a/packages/pwa-kit-react-sdk/package-lock.json +++ b/packages/pwa-kit-react-sdk/package-lock.json @@ -12,6 +12,13 @@ "@loadable/babel-plugin": "^5.15.3", "@loadable/server": "^5.15.3", "@loadable/webpack-plugin": "^5.15.2", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.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 +644,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", From c6c1d89216b506e45ea1fba9103a56b9f496839d Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Mon, 23 Jun 2025 13:33:07 -0700 Subject: [PATCH 003/626] Updated the open telemetry tests --- .../src/ssr/server/opentelemetry-server.test.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 index 6e5274d964..aeeb9253cc 100644 --- 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 @@ -53,6 +53,9 @@ describe('OpenTelemetry Server Tracing', () => { let mockResource let mockPropagation let consoleWarnSpy + let initializeServerTracing + let shutdownServerTracing + let isServerTracingInitialized beforeEach(() => { // Reset all mocks @@ -94,8 +97,16 @@ describe('OpenTelemetry Server Tracing', () => { mockB3Propagator.mockImplementation(() => mockB3PropagatorInstance) }) - afterEach(() => { + afterEach(async () => { consoleWarnSpy.mockRestore() + + // Clean up any existing provider + if (shutdownServerTracing) { + await shutdownServerTracing() + } + + // Reset module state to ensure clean state between tests + jest.resetModules() }) describe('initializeServerTracing', () => { From f03ca23e0e595e422790124b4ecc06346d273bde Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Mon, 23 Jun 2025 13:42:36 -0700 Subject: [PATCH 004/626] Linted open telemetry files --- .../src/ssr/server/opentelemetry-server.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index aeeb9253cc..4e0d06e487 100644 --- 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 @@ -99,12 +99,12 @@ describe('OpenTelemetry Server Tracing', () => { afterEach(async () => { consoleWarnSpy.mockRestore() - + // Clean up any existing provider if (shutdownServerTracing) { await shutdownServerTracing() } - + // Reset module state to ensure clean state between tests jest.resetModules() }) From 245f60d803d2a7366bd287c53d047ba004f019f4 Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Mon, 23 Jun 2025 14:40:01 -0700 Subject: [PATCH 005/626] Linted open telemetry files --- .../src/ssr/server/opentelemetry-server.js | 4 ++-- .../src/ssr/server/opentelemetry-server.test.js | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) 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 index 5eecfe5f74..f426ad460d 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js @@ -22,7 +22,7 @@ let provider = null */ export const initializeServerTracing = () => { try { - // Initialize the tracer provider + // Initialize the tracer provider provider = new NodeTracerProvider({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME @@ -71,4 +71,4 @@ export const getServerTracingProvider = () => { */ export const isServerTracingInitialized = () => { return provider !== null -} +} \ No newline at end of file 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 index 4e0d06e487..05634dcc0c 100644 --- 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 @@ -11,12 +11,6 @@ // That conflicts with the monorepo header rule, so we must disable the rule! /* eslint-disable header/header */ -import { - initializeServerTracing, - shutdownServerTracing, - isServerTracingInitialized -} from './opentelemetry-server' - // Mock OpenTelemetry dependencies jest.mock('@opentelemetry/sdk-trace-node', () => ({ NodeTracerProvider: jest.fn() @@ -56,6 +50,7 @@ describe('OpenTelemetry Server Tracing', () => { let initializeServerTracing let shutdownServerTracing let isServerTracingInitialized + let getServerTracingProvider beforeEach(() => { // Reset all mocks @@ -95,6 +90,13 @@ describe('OpenTelemetry Server Tracing', () => { mockResource.mockImplementation(() => mockResourceInstance) mockSimpleSpanProcessor.mockImplementation(() => mockSpanProcessorInstance) mockB3Propagator.mockImplementation(() => mockB3PropagatorInstance) + + // Import the functions after mocks are set up + const opentelemetryServer = require('./opentelemetry-server') + initializeServerTracing = opentelemetryServer.initializeServerTracing + shutdownServerTracing = opentelemetryServer.shutdownServerTracing + isServerTracingInitialized = opentelemetryServer.isServerTracingInitialized + getServerTracingProvider = opentelemetryServer.getServerTracingProvider }) afterEach(async () => { From a6d0bb717f9f40d63238859d58ee3900519d74ca Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Mon, 23 Jun 2025 14:46:48 -0700 Subject: [PATCH 006/626] Linted open telemetry files --- .../pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js | 4 ++-- .../src/ssr/server/opentelemetry-server.test.js | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) 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 index f426ad460d..5eecfe5f74 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js @@ -22,7 +22,7 @@ let provider = null */ export const initializeServerTracing = () => { try { - // Initialize the tracer provider + // Initialize the tracer provider provider = new NodeTracerProvider({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME @@ -71,4 +71,4 @@ export const getServerTracingProvider = () => { */ export const isServerTracingInitialized = () => { return provider !== null -} \ No newline at end of file +} 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 index 05634dcc0c..aee4b575da 100644 --- 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 @@ -50,7 +50,6 @@ describe('OpenTelemetry Server Tracing', () => { let initializeServerTracing let shutdownServerTracing let isServerTracingInitialized - let getServerTracingProvider beforeEach(() => { // Reset all mocks @@ -66,6 +65,7 @@ describe('OpenTelemetry Server Tracing', () => { const {B3Propagator} = require('@opentelemetry/propagator-b3') const {Resource} = require('@opentelemetry/resources') const {propagation} = require('@opentelemetry/api') + const opentelemetryServer = require('./opentelemetry-server') /* eslint-enable @typescript-eslint/no-var-requires */ mockNodeTracerProvider = NodeTracerProvider @@ -92,11 +92,9 @@ describe('OpenTelemetry Server Tracing', () => { mockB3Propagator.mockImplementation(() => mockB3PropagatorInstance) // Import the functions after mocks are set up - const opentelemetryServer = require('./opentelemetry-server') initializeServerTracing = opentelemetryServer.initializeServerTracing shutdownServerTracing = opentelemetryServer.shutdownServerTracing isServerTracingInitialized = opentelemetryServer.isServerTracingInitialized - getServerTracingProvider = opentelemetryServer.getServerTracingProvider }) afterEach(async () => { From dc48667553f8ad2f6fe1c1f77f2d6cc0759a438e Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Wed, 25 Jun 2025 10:37:23 -0700 Subject: [PATCH 007/626] updated open telemetry files --- packages/pwa-kit-react-sdk/package-lock.json | 1 - packages/pwa-kit-react-sdk/package.json | 1 - .../pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js | 2 +- .../src/ssr/server/opentelemetry-server.test.js | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/pwa-kit-react-sdk/package-lock.json b/packages/pwa-kit-react-sdk/package-lock.json index dbdbce174c..5ae23d0b10 100644 --- a/packages/pwa-kit-react-sdk/package-lock.json +++ b/packages/pwa-kit-react-sdk/package-lock.json @@ -13,7 +13,6 @@ "@loadable/server": "^5.15.3", "@loadable/webpack-plugin": "^5.15.2", "@opentelemetry/api": "^1.4.1", - "@opentelemetry/core": "^1.15.1", "@opentelemetry/propagator-b3": "^1.15.1", "@opentelemetry/resources": "^1.15.1", "@opentelemetry/sdk-trace-base": "^1.15.1", diff --git a/packages/pwa-kit-react-sdk/package.json b/packages/pwa-kit-react-sdk/package.json index b01d4b46db..d952ff2c49 100644 --- a/packages/pwa-kit-react-sdk/package.json +++ b/packages/pwa-kit-react-sdk/package.json @@ -38,7 +38,6 @@ "@loadable/server": "^5.15.3", "@loadable/webpack-plugin": "^5.15.2", "@opentelemetry/api": "^1.4.1", - "@opentelemetry/core": "^1.15.1", "@opentelemetry/propagator-b3": "^1.15.1", "@opentelemetry/resources": "^1.15.1", "@opentelemetry/sdk-trace-base": "^1.15.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 index 5eecfe5f74..2311a8fa9b 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Salesforce, Inc. + * 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 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 index aee4b575da..3c810ee0e3 100644 --- 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 @@ -2,7 +2,7 @@ * @jest-environment node */ /* - * Copyright (c) 2024, Salesforce, Inc. + * 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 From 40c3da91eb354735154fcd5b7cac72c147c01496 Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Thu, 26 Jun 2025 16:47:09 -0700 Subject: [PATCH 008/626] made changes to server and test files --- .../src/ssr/server/opentelemetry-server.js | 47 +++- .../ssr/server/opentelemetry-server.test.js | 217 +++++++++++++++--- 2 files changed, 219 insertions(+), 45 deletions(-) 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 index 2311a8fa9b..3c74936292 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js @@ -9,35 +9,61 @@ 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 {SemanticResourceAttributes} from '@opentelemetry/semantic-conventions' import {propagation} from '@opentelemetry/api' +import logger from '../../utils/logger-instance' -const SERVICE_NAME = 'pwa-kit-react-sdk' +const DEFAULT_SERVICE_NAME = 'pwa-kit-react-sdk' let provider = null /** * Initialize OpenTelemetry tracing for server-side rendering + * @param {Object} options - Configuration options for OpenTelemetry + * @param {string} [options.serviceName] - Service name for the resource (defaults to OTEL_SERVICE_NAME env var or DEFAULT_SERVICE_NAME) + * @param {string} [options.serviceVersion] - Service version (defaults to npm_package_version env var) + * @param {boolean} [options.enabled] - Whether to enable tracing (defaults to OTEL_SDK_ENABLED env var === 'true') * @returns {NodeTracerProvider|null} The initialized provider or null if initialization failed */ -export const initializeServerTracing = () => { +export const initializeServerTracing = (options = {}) => { + const { + serviceName = process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE_NAME, + serviceVersion = process.env.npm_package_version, + 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({ - [SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME - }) + resource: new Resource(resourceAttributes), + spanProcessor: new SimpleSpanProcessor() }) // Add B3 propagator - provider.addSpanProcessor(new SimpleSpanProcessor()) propagation.setGlobalPropagator(new B3Propagator()) provider.register() return provider } catch (error) { // Log errors from OpenTelemetry initialization - console.warn('Failed to initialize OpenTelemetry provider:', error.message) + logger.warn('Failed to initialize OpenTelemetry provider', { + namespace: 'opentelemetry-server.initializeServerTracing', + additionalProperties: {error: error.message} + }) return null } } @@ -52,7 +78,10 @@ export const shutdownServerTracing = async () => { await provider.shutdown() provider = null // Clean up after successful shutdown } catch (error) { - console.warn('Failed to shutdown OpenTelemetry provider:', error.message) + logger.warn('Failed to shutdown OpenTelemetry provider', { + namespace: 'opentelemetry-server.shutdownServerTracing', + additionalProperties: {error: error.message} + }) } } } 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 index 3c810ee0e3..663ad30da6 100644 --- 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 @@ -28,25 +28,26 @@ jest.mock('@opentelemetry/resources', () => ({ Resource: jest.fn() })) -jest.mock('@opentelemetry/semantic-conventions', () => ({ - SemanticResourceAttributes: { - SERVICE_NAME: 'service.name' - } -})) - 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 consoleWarnSpy + let mockLogger let initializeServerTracing let shutdownServerTracing let isServerTracingInitialized @@ -55,9 +56,6 @@ describe('OpenTelemetry Server Tracing', () => { // Reset all mocks jest.clearAllMocks() - // Set up console.warn spy - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}) - // Get mocked constructors /* eslint-disable @typescript-eslint/no-var-requires */ const {NodeTracerProvider} = require('@opentelemetry/sdk-trace-node') @@ -65,6 +63,7 @@ describe('OpenTelemetry Server Tracing', () => { 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 */ @@ -73,10 +72,10 @@ describe('OpenTelemetry Server Tracing', () => { mockB3Propagator = B3Propagator mockResource = Resource mockPropagation = propagation + mockLogger = logger // Set up mock instances const mockProviderInstance = { - addSpanProcessor: jest.fn(), register: jest.fn(), shutdown: jest.fn() } @@ -98,8 +97,6 @@ describe('OpenTelemetry Server Tracing', () => { }) afterEach(async () => { - consoleWarnSpy.mockRestore() - // Clean up any existing provider if (shutdownServerTracing) { await shutdownServerTracing() @@ -110,12 +107,13 @@ describe('OpenTelemetry Server Tracing', () => { }) describe('initializeServerTracing', () => { - test('should successfully initialize OpenTelemetry tracing', () => { + test('should successfully initialize OpenTelemetry tracing with default options', () => { const result = initializeServerTracing() - // Verify NodeTracerProvider was called with correct resource + // Verify NodeTracerProvider was called with correct resource and span processor expect(mockNodeTracerProvider).toHaveBeenCalledWith({ - resource: expect.any(Object) + resource: expect.any(Object), + spanProcessor: expect.any(Object) }) // Verify Resource was created with correct service name @@ -123,9 +121,8 @@ describe('OpenTelemetry Server Tracing', () => { 'service.name': 'pwa-kit-react-sdk' }) - // Verify span processor was added + // Verify span processor was created expect(mockSimpleSpanProcessor).toHaveBeenCalled() - expect(result.addSpanProcessor).toHaveBeenCalledWith(expect.any(Object)) // Verify B3 propagator was set globally expect(mockB3Propagator).toHaveBeenCalled() @@ -141,6 +138,58 @@ describe('OpenTelemetry Server Tracing', () => { expect(isServerTracingInitialized()).toBe(true) }) + test('should initialize with custom service name', () => { + const customServiceName = 'my-custom-service' + const result = initializeServerTracing({ serviceName: customServiceName }) + + // Verify Resource was created with custom service name + expect(mockResource).toHaveBeenCalledWith({ + 'service.name': customServiceName + }) + + expect(result).toBeDefined() + }) + + test('should initialize with service version', () => { + const serviceVersion = '1.2.3' + const result = initializeServerTracing({ 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({ + 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(() => { @@ -150,9 +199,12 @@ describe('OpenTelemetry Server Tracing', () => { const result = initializeServerTracing() // Verify error was logged - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Failed to initialize OpenTelemetry provider:', - 'OpenTelemetry initialization failed' + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider', + { + namespace: 'opentelemetry-server.initializeServerTracing', + additionalProperties: {error: 'OpenTelemetry initialization failed'} + } ) // Verify null was returned @@ -171,9 +223,12 @@ describe('OpenTelemetry Server Tracing', () => { const result = initializeServerTracing() // Verify error was logged - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Failed to initialize OpenTelemetry provider:', - 'Resource creation failed' + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider', + { + namespace: 'opentelemetry-server.initializeServerTracing', + additionalProperties: {error: 'Resource creation failed'} + } ) // Verify null was returned @@ -183,7 +238,6 @@ describe('OpenTelemetry Server Tracing', () => { test('should handle provider registration errors', () => { // Set up mock provider that throws on register const mockProviderInstance = { - addSpanProcessor: jest.fn(), register: jest.fn().mockImplementation(() => { throw new Error('Provider registration failed') }), @@ -194,14 +248,100 @@ describe('OpenTelemetry Server Tracing', () => { const result = initializeServerTracing() // Verify error was logged - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Failed to initialize OpenTelemetry provider:', - 'Provider registration failed' + expect(mockLogger.warn).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', () => { + process.env.OTEL_SERVICE_NAME = 'env-service-name' + + // Re-import to get fresh module with updated env + const opentelemetryServer = require('./opentelemetry-server') + const { initializeServerTracing } = opentelemetryServer + + const result = initializeServerTracing() + + expect(mockResource).toHaveBeenCalledWith({ + 'service.name': 'env-service-name' + }) + expect(result).toBeDefined() + }) + + test('should use npm_package_version environment variable when provided', () => { + process.env.npm_package_version = '3.0.0' + + // Re-import to get fresh module with updated env + const opentelemetryServer = require('./opentelemetry-server') + const { initializeServerTracing } = opentelemetryServer + + const result = initializeServerTracing() + + expect(mockResource).toHaveBeenCalledWith({ + 'service.name': 'pwa-kit-react-sdk', + 'service.version': '3.0.0' + }) + expect(result).toBeDefined() + }) + + test('should enable tracing when OTEL_SDK_ENABLED is true', () => { + process.env.OTEL_SDK_ENABLED = 'true' + + // Re-import to get fresh module with updated env + const opentelemetryServer = require('./opentelemetry-server') + const { initializeServerTracing } = opentelemetryServer + + 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 + const opentelemetryServer = require('./opentelemetry-server') + const { initializeServerTracing } = opentelemetryServer + + const result = initializeServerTracing() + + 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 + const opentelemetryServer = require('./opentelemetry-server') + const { initializeServerTracing } = opentelemetryServer + + const result = initializeServerTracing() + + expect(mockNodeTracerProvider).not.toHaveBeenCalled() + expect(result).toBeNull() + }) + }) }) describe('shutdownServerTracing', () => { @@ -227,13 +367,12 @@ describe('OpenTelemetry Server Tracing', () => { await shutdownServerTracing() // Verify no error was logged (graceful handling) - expect(consoleWarnSpy).not.toHaveBeenCalled() + expect(mockLogger.warn).not.toHaveBeenCalled() }) test('should handle shutdown errors gracefully', async () => { // Set up mock provider that throws on shutdown const mockProviderInstance = { - addSpanProcessor: jest.fn(), register: jest.fn(), shutdown: jest.fn().mockRejectedValue(new Error('Shutdown failed')) } @@ -247,9 +386,12 @@ describe('OpenTelemetry Server Tracing', () => { await expect(shutdownServerTracing()).resolves.not.toThrow() // Verify error was logged - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Failed to shutdown OpenTelemetry provider:', - 'Shutdown failed' + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Failed to shutdown OpenTelemetry provider', + { + namespace: 'opentelemetry-server.shutdownServerTracing', + additionalProperties: {error: 'Shutdown failed'} + } ) }) }) @@ -304,9 +446,12 @@ describe('OpenTelemetry Server Tracing', () => { const provider = initializeServerTracing() expect(provider).toBeNull() expect(isServerTracingInitialized()).toBe(false) - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Failed to initialize OpenTelemetry provider:', - 'Initialization failed' + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Failed to initialize OpenTelemetry provider', + { + namespace: 'opentelemetry-server.initializeServerTracing', + additionalProperties: {error: 'Initialization failed'} + } ) // Shutdown should still work gracefully From 6cc769a669567cde650ebe45e6fb9e180dd94b35 Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Fri, 27 Jun 2025 10:01:59 -0700 Subject: [PATCH 009/626] made changes to server and test files --- .../src/ssr/server/opentelemetry-server.js | 12 ++-- .../ssr/server/opentelemetry-server.test.js | 57 ++++++++++--------- 2 files changed, 37 insertions(+), 32 deletions(-) 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 index 3c74936292..06358caab5 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js @@ -18,11 +18,11 @@ let provider = null /** * Initialize OpenTelemetry tracing for server-side rendering - * @param {Object} options - Configuration options for OpenTelemetry - * @param {string} [options.serviceName] - Service name for the resource (defaults to OTEL_SERVICE_NAME env var or DEFAULT_SERVICE_NAME) - * @param {string} [options.serviceVersion] - Service version (defaults to npm_package_version env var) - * @param {boolean} [options.enabled] - Whether to enable tracing (defaults to OTEL_SDK_ENABLED env var === 'true') - * @returns {NodeTracerProvider|null} The initialized provider or null if initialization failed + * @param {Object} options + * @param {string} [options.serviceName] + * @param {string} [options.serviceVersion] + * @param {boolean} [options.enabled] + * @returns {NodeTracerProvider|null} */ export const initializeServerTracing = (options = {}) => { const { @@ -60,7 +60,7 @@ export const initializeServerTracing = (options = {}) => { return provider } catch (error) { // Log errors from OpenTelemetry initialization - logger.warn('Failed to initialize OpenTelemetry provider', { + logger.error('Failed to initialize OpenTelemetry provider', { namespace: 'opentelemetry-server.initializeServerTracing', additionalProperties: {error: error.message} }) 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 index 663ad30da6..44ad2ade86 100644 --- 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 @@ -140,7 +140,7 @@ describe('OpenTelemetry Server Tracing', () => { test('should initialize with custom service name', () => { const customServiceName = 'my-custom-service' - const result = initializeServerTracing({ serviceName: customServiceName }) + const result = initializeServerTracing({serviceName: customServiceName}) // Verify Resource was created with custom service name expect(mockResource).toHaveBeenCalledWith({ @@ -152,7 +152,7 @@ describe('OpenTelemetry Server Tracing', () => { test('should initialize with service version', () => { const serviceVersion = '1.2.3' - const result = initializeServerTracing({ serviceVersion }) + const result = initializeServerTracing({serviceVersion}) // Verify Resource was created with service version expect(mockResource).toHaveBeenCalledWith({ @@ -166,9 +166,9 @@ describe('OpenTelemetry Server Tracing', () => { test('should initialize with both service name and version', () => { const customServiceName = 'my-service' const serviceVersion = '2.0.0' - const result = initializeServerTracing({ - serviceName: customServiceName, - serviceVersion + const result = initializeServerTracing({ + serviceName: customServiceName, + serviceVersion }) // Verify Resource was created with both attributes @@ -181,7 +181,7 @@ describe('OpenTelemetry Server Tracing', () => { }) test('should return null when tracing is disabled', () => { - const result = initializeServerTracing({ enabled: false }) + const result = initializeServerTracing({enabled: false}) // Verify no provider was created expect(mockNodeTracerProvider).not.toHaveBeenCalled() @@ -199,7 +199,7 @@ describe('OpenTelemetry Server Tracing', () => { const result = initializeServerTracing() // Verify error was logged - expect(mockLogger.warn).toHaveBeenCalledWith( + expect(mockLogger.error).toHaveBeenCalledWith( 'Failed to initialize OpenTelemetry provider', { namespace: 'opentelemetry-server.initializeServerTracing', @@ -223,7 +223,7 @@ describe('OpenTelemetry Server Tracing', () => { const result = initializeServerTracing() // Verify error was logged - expect(mockLogger.warn).toHaveBeenCalledWith( + expect(mockLogger.error).toHaveBeenCalledWith( 'Failed to initialize OpenTelemetry provider', { namespace: 'opentelemetry-server.initializeServerTracing', @@ -248,7 +248,7 @@ describe('OpenTelemetry Server Tracing', () => { const result = initializeServerTracing() // Verify error was logged - expect(mockLogger.warn).toHaveBeenCalledWith( + expect(mockLogger.error).toHaveBeenCalledWith( 'Failed to initialize OpenTelemetry provider', { namespace: 'opentelemetry-server.initializeServerTracing', @@ -265,7 +265,7 @@ describe('OpenTelemetry Server Tracing', () => { beforeEach(() => { jest.resetModules() - process.env = { ...originalEnv } + process.env = {...originalEnv} }) afterEach(() => { @@ -274,11 +274,12 @@ describe('OpenTelemetry Server Tracing', () => { test('should use OTEL_SERVICE_NAME environment variable when provided', () => { process.env.OTEL_SERVICE_NAME = 'env-service-name' - + // 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 {initializeServerTracing} = opentelemetryServer + const result = initializeServerTracing() expect(mockResource).toHaveBeenCalledWith({ @@ -289,11 +290,12 @@ describe('OpenTelemetry Server Tracing', () => { test('should use npm_package_version environment variable when provided', () => { process.env.npm_package_version = '3.0.0' - + // 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 {initializeServerTracing} = opentelemetryServer + const result = initializeServerTracing() expect(mockResource).toHaveBeenCalledWith({ @@ -305,11 +307,12 @@ describe('OpenTelemetry Server Tracing', () => { test('should enable tracing when OTEL_SDK_ENABLED is true', () => { process.env.OTEL_SDK_ENABLED = 'true' - + // 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 {initializeServerTracing} = opentelemetryServer + const result = initializeServerTracing() expect(mockNodeTracerProvider).toHaveBeenCalled() @@ -318,11 +321,12 @@ describe('OpenTelemetry Server Tracing', () => { 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 {initializeServerTracing} = opentelemetryServer + const result = initializeServerTracing() expect(mockNodeTracerProvider).not.toHaveBeenCalled() @@ -331,11 +335,12 @@ describe('OpenTelemetry Server Tracing', () => { 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 {initializeServerTracing} = opentelemetryServer + const result = initializeServerTracing() expect(mockNodeTracerProvider).not.toHaveBeenCalled() @@ -446,7 +451,7 @@ describe('OpenTelemetry Server Tracing', () => { const provider = initializeServerTracing() expect(provider).toBeNull() expect(isServerTracingInitialized()).toBe(false) - expect(mockLogger.warn).toHaveBeenCalledWith( + expect(mockLogger.error).toHaveBeenCalledWith( 'Failed to initialize OpenTelemetry provider', { namespace: 'opentelemetry-server.initializeServerTracing', From c2f1b91243edd74d53fce9329946d7b2f2c6f2ce Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Fri, 27 Jun 2025 11:40:16 -0700 Subject: [PATCH 010/626] made changes to test files --- .../ssr/server/opentelemetry-server.test.js | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) 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 index 44ad2ade86..e58fd64eae 100644 --- 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 @@ -51,6 +51,7 @@ describe('OpenTelemetry Server Tracing', () => { let initializeServerTracing let shutdownServerTracing let isServerTracingInitialized + let defaultOptions beforeEach(() => { // Reset all mocks @@ -73,6 +74,7 @@ describe('OpenTelemetry Server Tracing', () => { mockResource = Resource mockPropagation = propagation mockLogger = logger + defaultOptions = {enabled: true} // Set up mock instances const mockProviderInstance = { @@ -108,7 +110,7 @@ describe('OpenTelemetry Server Tracing', () => { describe('initializeServerTracing', () => { test('should successfully initialize OpenTelemetry tracing with default options', () => { - const result = initializeServerTracing() + const result = initializeServerTracing(defaultOptions) // Verify NodeTracerProvider was called with correct resource and span processor expect(mockNodeTracerProvider).toHaveBeenCalledWith({ @@ -118,7 +120,8 @@ describe('OpenTelemetry Server Tracing', () => { // Verify Resource was created with correct service name expect(mockResource).toHaveBeenCalledWith({ - 'service.name': 'pwa-kit-react-sdk' + 'service.name': 'pwa-kit-react-sdk', + 'service.version': '3.11.0-dev.0' }) // Verify span processor was created @@ -140,11 +143,15 @@ describe('OpenTelemetry Server Tracing', () => { test('should initialize with custom service name', () => { const customServiceName = 'my-custom-service' - const result = initializeServerTracing({serviceName: customServiceName}) + const result = initializeServerTracing({ + ...defaultOptions, + serviceName: customServiceName + }) // Verify Resource was created with custom service name expect(mockResource).toHaveBeenCalledWith({ - 'service.name': customServiceName + 'service.name': customServiceName, + 'service.version': '3.11.0-dev.0' }) expect(result).toBeDefined() @@ -152,7 +159,7 @@ describe('OpenTelemetry Server Tracing', () => { test('should initialize with service version', () => { const serviceVersion = '1.2.3' - const result = initializeServerTracing({serviceVersion}) + const result = initializeServerTracing({...defaultOptions, serviceVersion}) // Verify Resource was created with service version expect(mockResource).toHaveBeenCalledWith({ @@ -167,6 +174,7 @@ describe('OpenTelemetry Server Tracing', () => { const customServiceName = 'my-service' const serviceVersion = '2.0.0' const result = initializeServerTracing({ + ...defaultOptions, serviceName: customServiceName, serviceVersion }) @@ -196,7 +204,7 @@ describe('OpenTelemetry Server Tracing', () => { throw new Error('OpenTelemetry initialization failed') }) - const result = initializeServerTracing() + const result = initializeServerTracing(defaultOptions) // Verify error was logged expect(mockLogger.error).toHaveBeenCalledWith( @@ -220,7 +228,7 @@ describe('OpenTelemetry Server Tracing', () => { throw new Error('Resource creation failed') }) - const result = initializeServerTracing() + const result = initializeServerTracing(defaultOptions) // Verify error was logged expect(mockLogger.error).toHaveBeenCalledWith( @@ -245,7 +253,7 @@ describe('OpenTelemetry Server Tracing', () => { } mockNodeTracerProvider.mockImplementation(() => mockProviderInstance) - const result = initializeServerTracing() + const result = initializeServerTracing(defaultOptions) // Verify error was logged expect(mockLogger.error).toHaveBeenCalledWith( @@ -273,30 +281,29 @@ describe('OpenTelemetry Server Tracing', () => { }) test('should use OTEL_SERVICE_NAME environment variable when provided', () => { - process.env.OTEL_SERVICE_NAME = 'env-service-name' + // Clear previous mock calls + mockResource.mockClear() + mockNodeTracerProvider.mockClear() - // 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 + process.env.OTEL_SERVICE_NAME = 'env-service-name' - const result = initializeServerTracing() + const result = initializeServerTracing({enabled: true}) expect(mockResource).toHaveBeenCalledWith({ - 'service.name': 'env-service-name' + 'service.name': 'env-service-name', + 'service.version': '3.11.0-dev.0' }) expect(result).toBeDefined() }) test('should use npm_package_version environment variable when provided', () => { - process.env.npm_package_version = '3.0.0' + // Clear previous mock calls + mockResource.mockClear() + mockNodeTracerProvider.mockClear() - // 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 + process.env.npm_package_version = '3.0.0' - const result = initializeServerTracing() + const result = initializeServerTracing({enabled: true}) expect(mockResource).toHaveBeenCalledWith({ 'service.name': 'pwa-kit-react-sdk', @@ -306,12 +313,11 @@ describe('OpenTelemetry Server Tracing', () => { }) test('should enable tracing when OTEL_SDK_ENABLED is true', () => { - process.env.OTEL_SDK_ENABLED = 'true' + // Clear previous mock calls + mockResource.mockClear() + mockNodeTracerProvider.mockClear() - // 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 + process.env.OTEL_SDK_ENABLED = 'true' const result = initializeServerTracing() @@ -327,7 +333,7 @@ describe('OpenTelemetry Server Tracing', () => { const opentelemetryServer = require('./opentelemetry-server') const {initializeServerTracing} = opentelemetryServer - const result = initializeServerTracing() + const result = initializeServerTracing(defaultOptions) expect(mockNodeTracerProvider).not.toHaveBeenCalled() expect(result).toBeNull() @@ -341,7 +347,7 @@ describe('OpenTelemetry Server Tracing', () => { const opentelemetryServer = require('./opentelemetry-server') const {initializeServerTracing} = opentelemetryServer - const result = initializeServerTracing() + const result = initializeServerTracing(defaultOptions) expect(mockNodeTracerProvider).not.toHaveBeenCalled() expect(result).toBeNull() @@ -352,7 +358,7 @@ describe('OpenTelemetry Server Tracing', () => { describe('shutdownServerTracing', () => { test('should successfully shutdown OpenTelemetry provider when provider exists', async () => { // First initialize to create a provider - const provider = initializeServerTracing() + const provider = initializeServerTracing(defaultOptions) expect(provider).toBeDefined() // Then shutdown @@ -384,7 +390,7 @@ describe('OpenTelemetry Server Tracing', () => { mockNodeTracerProvider.mockImplementation(() => mockProviderInstance) // Initialize to create provider - const provider = initializeServerTracing() + const provider = initializeServerTracing(defaultOptions) expect(provider).toBeDefined() // Shutdown should not throw @@ -403,7 +409,7 @@ describe('OpenTelemetry Server Tracing', () => { describe('isServerTracingInitialized', () => { test('should return true when provider is initialized', () => { - initializeServerTracing() + initializeServerTracing(defaultOptions) expect(isServerTracingInitialized()).toBe(true) }) @@ -412,7 +418,7 @@ describe('OpenTelemetry Server Tracing', () => { }) test('should return false after shutdown', async () => { - initializeServerTracing() + initializeServerTracing(defaultOptions) expect(isServerTracingInitialized()).toBe(true) await shutdownServerTracing() @@ -423,7 +429,7 @@ describe('OpenTelemetry Server Tracing', () => { describe('integration scenarios', () => { test('should handle complete lifecycle: initialize -> shutdown', async () => { // Initialize - const provider = initializeServerTracing() + const provider = initializeServerTracing(defaultOptions) expect(provider).toBeDefined() expect(isServerTracingInitialized()).toBe(true) @@ -448,7 +454,7 @@ describe('OpenTelemetry Server Tracing', () => { }) // Initialize should fail - const provider = initializeServerTracing() + const provider = initializeServerTracing(defaultOptions) expect(provider).toBeNull() expect(isServerTracingInitialized()).toBe(false) expect(mockLogger.error).toHaveBeenCalledWith( From 9130b9af93e3f36d051b0511c79513db1ddd2fd9 Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Mon, 30 Jun 2025 13:05:12 -0700 Subject: [PATCH 011/626] made changes to test and server files --- .../src/ssr/server/opentelemetry-server.js | 2 +- .../ssr/server/opentelemetry-server.test.js | 29 ++++--------------- 2 files changed, 6 insertions(+), 25 deletions(-) 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 index 06358caab5..1d6523ae7c 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/opentelemetry-server.js @@ -27,7 +27,7 @@ let provider = null export const initializeServerTracing = (options = {}) => { const { serviceName = process.env.OTEL_SERVICE_NAME || DEFAULT_SERVICE_NAME, - serviceVersion = process.env.npm_package_version, + serviceVersion, enabled = process.env.OTEL_SDK_ENABLED === 'true' } = options 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 index e58fd64eae..93c452965b 100644 --- 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 @@ -118,10 +118,9 @@ describe('OpenTelemetry Server Tracing', () => { spanProcessor: expect.any(Object) }) - // Verify Resource was created with correct service name + // Verify Resource was created with correct service name only (no version by default) expect(mockResource).toHaveBeenCalledWith({ - 'service.name': 'pwa-kit-react-sdk', - 'service.version': '3.11.0-dev.0' + 'service.name': 'pwa-kit-react-sdk' }) // Verify span processor was created @@ -148,10 +147,9 @@ describe('OpenTelemetry Server Tracing', () => { serviceName: customServiceName }) - // Verify Resource was created with custom service name + // Verify Resource was created with custom service name only expect(mockResource).toHaveBeenCalledWith({ - 'service.name': customServiceName, - 'service.version': '3.11.0-dev.0' + 'service.name': customServiceName }) expect(result).toBeDefined() @@ -290,24 +288,7 @@ describe('OpenTelemetry Server Tracing', () => { const result = initializeServerTracing({enabled: true}) expect(mockResource).toHaveBeenCalledWith({ - 'service.name': 'env-service-name', - 'service.version': '3.11.0-dev.0' - }) - expect(result).toBeDefined() - }) - - test('should use npm_package_version environment variable when provided', () => { - // Clear previous mock calls - mockResource.mockClear() - mockNodeTracerProvider.mockClear() - - process.env.npm_package_version = '3.0.0' - - const result = initializeServerTracing({enabled: true}) - - expect(mockResource).toHaveBeenCalledWith({ - 'service.name': 'pwa-kit-react-sdk', - 'service.version': '3.0.0' + 'service.name': 'env-service-name' }) expect(result).toBeDefined() }) From 7ae5702b4ef8e8e15ac0907491efe745f348f12f Mon Sep 17 00:00:00 2001 From: snilakandan Date: Tue, 1 Jul 2025 14:46:00 -0500 Subject: [PATCH 012/626] Added cursor rules --- .../README.md | 50 +++ .../product-detail-component.mdc | 158 +++++++ .../product-list-component.mdc | 398 ++++++++++++++++++ .../product-tile-component.mdc | 303 +++++++++++++ .../rules/product-component-requirements.mdc | 33 ++ 5 files changed, 942 insertions(+) create mode 100644 .cursor/rules/product-component-requirements-mdc/README.md create mode 100644 .cursor/rules/product-component-requirements-mdc/product-detail-component.mdc create mode 100644 .cursor/rules/product-component-requirements-mdc/product-list-component.mdc create mode 100644 .cursor/rules/product-component-requirements-mdc/product-tile-component.mdc create mode 100644 .cursor/rules/product-component-requirements.mdc diff --git a/.cursor/rules/product-component-requirements-mdc/README.md b/.cursor/rules/product-component-requirements-mdc/README.md new file mode 100644 index 0000000000..67a16e92d1 --- /dev/null +++ b/.cursor/rules/product-component-requirements-mdc/README.md @@ -0,0 +1,50 @@ +# Product Component Development Guide + +## 🚀 Getting Started (Beginner-Friendly) + +### Quick Start Path +1. **Start Simple**: Use `product-tile-component.mdc` for basic product displays +2. **Add Lists**: Move to `product-list-component.mdc` when you need collections +3. **Go Deep**: Use `product-detail-component.mdc` for full product pages + +### For New Developers +- Focus on one component type at a time +- Start with the "Basic Implementation" examples +- Skip advanced features until you're comfortable with basics +- Use the "Common Mistakes" sections to avoid pitfalls + +### When You're Ready for More +- Explore "Advanced Features" sections +- Study the "Best Practices" examples +- Review "Testing Guidelines" for quality assurance + +## 📚 Component Complexity Levels + +| Component | Complexity | Best For | +|-----------|------------|----------| +| Product Tile | ⭐ Beginner | Simple product cards, grids | +| Product List | ⭐⭐ Intermediate | Product collections, search results | +| Product Detail | ⭐⭐⭐ Advanced | Full product pages, complex layouts | + +## 🎯 Quick Reference + +### Product Tile +- **Use when**: Displaying products in grids, carousels, or lists +- **Key features**: Image, name, price, quick actions +- **Start here**: Perfect for beginners + +### Product List +- **Use when**: Showing multiple products with filtering/sorting +- **Key features**: Grid/list views, pagination, filters +- **Prerequisites**: Comfortable with Product Tiles + +### Product Detail +- **Use when**: Full product information pages +- **Key features**: Complete product data, variants, add to cart +- **Prerequisites**: Experience with Tiles and Lists + +## 💡 Tips for Success +- Copy the basic examples and modify gradually +- Test frequently as you build +- Ask for help when stuck - these are complex components! +- Start with mock data before integrating real APIs \ No newline at end of file diff --git a/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc new file mode 100644 index 0000000000..4a045074fb --- /dev/null +++ b/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc @@ -0,0 +1,158 @@ +--- +description: Product Detail component requirement template +globs: "**/components/**" +type: agent-requested +alwaysApply: false +--- + +# Product Detail Component + +## Data Model +- Product +- Product Bundle (parent with child items; only parent can be added to cart) +- Product Set (logical group; all items added to cart together, no parent/child) + +## Features/Props +- Show product images (gallery) +- Show product name, price, description +- Add to Cart (single, bundle parent, or set) +- Quantity Selector (for product or bundle parent; for sets, quantity per item if supported) +- Show Reviews +- Show Affinity/Recommendations +- Variant selection (color, size, etc.) +- Show all items in bundle/set with details + +## Look & Feel +- Theme +- Skeleton +- Style + +## Example Requirement Object +```js +const requirement = { + dataModel: 'Product' | 'ProductBundle' | 'ProductSet', + features: ['Gallery', 'Add to Cart', 'Quantity Selector', 'Show Reviews', 'Variant Selection', 'Show Bundle/Set Items'], + lookAndFeel: ['Theme', 'Skeleton', 'Style'] +} +``` + +## Example Usage +```jsx + +``` + +## Best Practices +- For bundles: Only allow the parent product to be added to cart; display all child items with details, but do not allow adding/removing them individually. +- For sets: Display all items in the set; when the set is added to cart, all items are added together. There is no parent/child distinction. +- Use dedicated subcomponents for gallery, reviews, and variant selection. +- Scaffold all features as configurable props. +- Use skeletons for loading states. +- Support theming and custom styles. +- Validate all props and document requirements. +- Ensure accessibility (alt text, keyboard navigation, ARIA labels). + +## Bad Practices +- For bundles: Allowing child items to be added to cart individually, or not showing all child items. +- For sets: Treating one item as a parent, or not adding all items to cart together. +- Mixing all logic in one large component (no subcomponents). +- Hardcoding feature logic (not making features optional/configurable). +- Ignoring loading states (no skeletons or spinners). +- Not supporting theming or custom styles. +- Skipping accessibility (missing alt text, no keyboard support). +- Not validating props or documenting requirements. + +## Example Component Skeleton +```jsx +/* +Requirement: +{ + dataModel: 'Product' | 'ProductBundle' | 'ProductSet', + features: ['Gallery', 'Add to Cart', 'Quantity Selector', 'Show Reviews', 'Variant Selection', 'Show Bundle/Set Items'], + lookAndFeel: ['Theme', 'Skeleton', 'Style'] +} +*/ + +import React, {useState} from 'react' +import PropTypes from 'prop-types' +import {Box, Text, Button, Skeleton, useTheme} from '@salesforce/retail-react-app/app/components/shared/ui' +import {QuantityPicker} from '@salesforce/retail-react-app/app/components/quantity-picker' +import RecommendedProducts from '@salesforce/retail-react-app/app/components/recommended-products' +// import ProductGallery from './ProductGallery' +// import ProductReviews from './ProductReviews' +// import VariantSelector from './VariantSelector' + +const ProductDetail = ({product, onAddToCart, showReviews = true, showRecommendations = true}) => { + const [quantity, setQuantity] = useState(1) + const [isLoading, setIsLoading] = useState(false) + const theme = useTheme() + + const handleAddToCart = async () => { + setIsLoading(true) + await onAddToCart(product, quantity) + setIsLoading(false) + } + + // Example: Render bundle/set items + const renderBundleOrSetItems = () => { + if (product.type === 'bundle' && product.childItems) { + return ( + + Bundle Includes: + {product.childItems.map((item) => ( + + {item.name} + {/* No add to cart for child items */} + + ))} + + ) + } + if (product.type === 'set' && product.setItems) { + return ( + + Set Includes: + {product.setItems.map((item) => ( + + {item.name} + {/* All items in set are added to cart together */} + + ))} + + ) + } + return null + } + + return ( + + + {/* */} + {product.name} + {product.description} + {/* */} + + + {renderBundleOrSetItems()} + {showReviews && ( + // + Reviews go here + )} + {showRecommendations && ( + + )} + + + ) +} + +ProductDetail.propTypes = { + product: PropTypes.object.isRequired, + onAddToCart: PropTypes.func.isRequired, + showReviews: PropTypes.bool, + showRecommendations: PropTypes.bool +} + +export default ProductDetail +``` diff --git a/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc new file mode 100644 index 0000000000..9e45a7e2d9 --- /dev/null +++ b/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc @@ -0,0 +1,398 @@ +--- +description: Product list component requirement template +globs: "**/components/**" +type: agent-requested +alwaysApply: false +--- +# Product List Component + +## Data Model +- Array of Products (single items) +- Array of Product Bundles (parent with child items; only parent can be added to cart) +- Array of Product Sets (logical groups; all items added to cart together, no parent/child) +- Mixed array (products, bundles, and sets together) + +## Features/Props +- Grid/List view toggle +- Sorting options (price, name, rating, newest, popularity) +- Filtering (category, price range, rating, availability) +- Pagination or infinite scroll +- Search functionality +- Loading states and skeletons +- Empty state handling +- Product count display +- View mode persistence +- Responsive grid/list layouts +- Bulk actions (add multiple to cart, wishlist) + +## Look & Feel +- Theme +- Skeleton +- Style (grid/list view options) +- Responsive design +- Loading animations +- Empty state design + +## Example Requirement Object +```js +const requirement = { + dataModel: 'Product[]' | 'ProductBundle[]' | 'ProductSet[]' | 'Mixed[]', + features: ['Grid/List Toggle', 'Sorting', 'Filtering', 'Pagination', 'Search', 'Loading States', 'Empty State', 'Product Count', 'View Persistence', 'Responsive Layout', 'Bulk Actions'], + lookAndFeel: ['Theme', 'Skeleton', 'Style', 'Responsive', 'Loading Animations', 'Empty State Design'] +} +``` + +## Example Usage +```jsx + +``` + +## Best Practices +- Support both grid and list view modes with smooth transitions. +- Implement proper sorting with clear visual indicators. +- Provide comprehensive filtering with clear active filter display. +- Use infinite scroll or pagination based on performance requirements. +- Show loading skeletons for better perceived performance. +- Handle empty states gracefully with helpful messaging. +- Display product count and current view information. +- Persist user preferences (view mode, sort order, filters). +- Ensure responsive design works on all screen sizes. +- Implement proper accessibility (keyboard navigation, screen readers). +- Use virtualization for large product lists to improve performance. +- Validate all props and document requirements. + +## Bad Practices +- No view mode toggle or poor transitions between modes. +- Limited sorting options or unclear sort indicators. +- No filtering capabilities or poor filter UX. +- No pagination/infinite scroll for large lists. +- No loading states (poor perceived performance). +- Poor empty state handling (no helpful messaging). +- No product count or view information. +- Not persisting user preferences. +- Non-responsive design (breaks on mobile). +- Ignoring accessibility (no keyboard support, missing ARIA labels). +- Loading all products at once (no virtualization for large lists). +- Not validating props or documenting requirements. + +## Example Component Skeleton +```jsx +/* +Requirement: +{ + dataModel: 'Product[]' | 'ProductBundle[]' | 'ProductSet[]' | 'Mixed[]', + features: ['Grid/List Toggle', 'Sorting', 'Filtering', 'Pagination', 'Search', 'Loading States', 'Empty State', 'Product Count', 'View Persistence', 'Responsive Layout', 'Bulk Actions'], + lookAndFeel: ['Theme', 'Skeleton', 'Style', 'Responsive', 'Loading Animations', 'Empty State Design'] +} +*/ + +import React, {useState, useEffect, useMemo} from 'react' +import PropTypes from 'prop-types' +import {Box, Text, Button, Skeleton, useTheme, Select, HStack, VStack, Grid, Flex, Spinner} from '@salesforce/retail-react-app/app/components/shared/ui' +import {Grid as GridIcon, List as ListIcon, Filter, Search} from '@salesforce/retail-react-app/app/components/shared/icons' +import ProductTile from './ProductTile' +// import ProductListTile from './ProductListTile' +// import ProductFilters from './ProductFilters' +// import ProductSearch from './ProductSearch' + +const ProductList = ({ + products = [], + onAddToCart, + onWishlistToggle, + onQuickView, + viewMode: initialViewMode = 'grid', + sortBy: initialSortBy = 'name', + filters = {}, + onLoadMore, + isLoading = false, + hasMore = false, + showFilters = true, + showSearch = true, + showSort = true, + showViewToggle = true, + itemsPerPage = 12 +}) => { + const [viewMode, setViewMode] = useState(initialViewMode) + const [sortBy, setSortBy] = useState(initialSortBy) + const [currentFilters, setCurrentFilters] = useState(filters) + const [searchQuery, setSearchQuery] = useState('') + const [currentPage, setCurrentPage] = useState(1) + const theme = useTheme() + + // Persist view mode preference + useEffect(() => { + localStorage.setItem('productListViewMode', viewMode) + }, [viewMode]) + + // Load view mode preference + useEffect(() => { + const savedViewMode = localStorage.getItem('productListViewMode') + if (savedViewMode) setViewMode(savedViewMode) + }, []) + + // Sort and filter products + const processedProducts = useMemo(() => { + let filtered = products.filter(product => { + // Search filter + if (searchQuery && !product.name.toLowerCase().includes(searchQuery.toLowerCase())) { + return false + } + + // Apply other filters (category, price range, etc.) + if (currentFilters.category && product.category !== currentFilters.category) { + return false + } + if (currentFilters.minPrice && product.price < currentFilters.minPrice) { + return false + } + if (currentFilters.maxPrice && product.price > currentFilters.maxPrice) { + return false + } + if (currentFilters.inStock && product.stock === 0) { + return false + } + + return true + }) + + // Sort products + filtered.sort((a, b) => { + switch (sortBy) { + case 'price': + return a.price - b.price + case 'price-desc': + return b.price - a.price + case 'name': + return a.name.localeCompare(b.name) + case 'rating': + return (b.rating || 0) - (a.rating || 0) + case 'newest': + return new Date(b.createdAt) - new Date(a.createdAt) + case 'popularity': + return (b.salesCount || 0) - (a.salesCount || 0) + default: + return 0 + } + }) + + return filtered + }, [products, searchQuery, currentFilters, sortBy]) + + // Paginate products + const paginatedProducts = useMemo(() => { + const startIndex = (currentPage - 1) * itemsPerPage + return processedProducts.slice(startIndex, startIndex + itemsPerPage) + }, [processedProducts, currentPage, itemsPerPage]) + + const handleLoadMore = () => { + if (hasMore && onLoadMore) { + onLoadMore() + } else { + setCurrentPage(prev => prev + 1) + } + } + + const handleFilterChange = (newFilters) => { + setCurrentFilters(newFilters) + setCurrentPage(1) // Reset to first page when filters change + } + + const renderEmptyState = () => ( + + + No products found + + + Try adjusting your search or filters to find what you're looking for. + + + + ) + + const renderProductGrid = () => ( + + {paginatedProducts.map((product) => ( + + ))} + + ) + + const renderProductList = () => ( + + {paginatedProducts.map((product) => ( + // + + {product.name} - ${product.price} + + ))} + + ) + + return ( + + {/* Header with controls */} + + + + Products ({processedProducts.length}) + + + + {/* Search */} + {showSearch && ( + + + setSearchQuery(e.target.value)} + style={{ + padding: '8px 12px 8px 36px', + border: '1px solid #e2e8f0', + borderRadius: '6px', + width: '250px' + }} + /> + + )} + + {/* Sort */} + {showSort && ( + + )} + + {/* View Toggle */} + {showViewToggle && ( + + + + + )} + + + + {/* Filters */} + {showFilters && ( + + {/* */} + + Filters: {Object.keys(currentFilters).length > 0 ? + Object.entries(currentFilters).map(([key, value]) => `${key}: ${value}`).join(', ') : + 'None applied' + } + + + )} + + + {/* Product Grid/List */} + + {processedProducts.length === 0 ? ( + renderEmptyState() + ) : ( + <> + {viewMode === 'grid' ? renderProductGrid() : renderProductList()} + + {/* Load More / Pagination */} + {(hasMore || currentPage * itemsPerPage < processedProducts.length) && ( + + + + )} + + )} + + + ) +} + +ProductList.propTypes = { + products: PropTypes.array.isRequired, + onAddToCart: PropTypes.func.isRequired, + onWishlistToggle: PropTypes.func, + onQuickView: PropTypes.func, + viewMode: PropTypes.oneOf(['grid', 'list']), + sortBy: PropTypes.string, + filters: PropTypes.object, + onLoadMore: PropTypes.func, + isLoading: PropTypes.bool, + hasMore: PropTypes.bool, + showFilters: PropTypes.bool, + showSearch: PropTypes.bool, + showSort: PropTypes.bool, + showViewToggle: PropTypes.bool, + itemsPerPage: PropTypes.number +} + +export default ProductList diff --git a/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc new file mode 100644 index 0000000000..03fb94e584 --- /dev/null +++ b/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc @@ -0,0 +1,303 @@ +--- +description: Product Tile component requirement template +globs: "**/components/**" +type: agent-requested +alwaysApply: false +--- +name: Product Tile Component +description: Requirements and examples for creating product tile components (cards, grid items) +globs: ["**/components/**"] + +# Product Tile Component + +## 🚀 Quick Start (Beginner) +If you're new to product components, start here: + +### Basic Product Tile +```jsx +const ProductTile = ({ product, onAddToCart }) => { + return ( +
+ {product.name} +

{product.name}

+

{product.price}

+ +
+ ) +} +``` + +### Essential Props +- `product`: Product data object +- `onAddToCart`: Function to add product to cart +- `className`: Optional styling + +--- + +## 📋 Full Requirements (Advanced) + +### Data Model +- Product (single item) +- Product Bundle (parent with child items; only parent can be added to cart) +- Product Set (logical group; all items added to cart together, no parent/child) + +## Features/Props +- Product image (primary image with optional hover gallery) +- Product name and price +- Quick Add to Cart (single, bundle parent, or set) +- Wishlist/Favorite toggle +- Quick view modal trigger +- Rating/Reviews display +- Sale/Badge indicators +- Stock status +- Variant preview (color swatches, size preview) +- Bundle/Set item count indicator + +## Look & Feel +- Theme +- Skeleton +- Style (grid/list view options) +- Hover effects +- Responsive design + +## Example Requirement Object +```js +const requirement = { + dataModel: 'Product' | 'ProductBundle' | 'ProductSet', + features: ['Image', 'Quick Add to Cart', 'Wishlist', 'Quick View', 'Rating', 'Sale Badge', 'Stock Status', 'Variant Preview', 'Bundle/Set Indicator'], + lookAndFeel: ['Theme', 'Skeleton', 'Style', 'Hover Effects', 'Responsive'] +} +``` + +## Example Usage +```jsx + +``` + +## Best Practices +- For bundles: Show bundle indicator and item count; only allow adding the parent to cart. +- For sets: Show set indicator and item count; add all items to cart together. +- Use lazy loading for images with proper alt text. +- Implement hover effects for better UX (image gallery, quick actions). +- Show stock status clearly (in stock, low stock, out of stock). +- Display sale badges prominently when applicable. +- Use skeleton loading states for better perceived performance. +- Support both grid and list view modes. +- Ensure accessibility (keyboard navigation, screen reader support). +- Validate all props and document requirements. + +## Bad Practices +- For bundles: Allowing child items to be added individually, or not showing bundle indicators. +- For sets: Treating one item as parent, or not adding all items together. +- Loading all images at once (no lazy loading). +- No hover states or interactive feedback. +- Missing stock status or sale indicators. +- No loading states (poor perceived performance). +- Hardcoded layout (no responsive design). +- Ignoring accessibility (no keyboard support, missing ARIA labels). +- Not validating props or documenting requirements. + +## Example Component Skeleton +```jsx +/* +Requirement: +{ + dataModel: 'Product' | 'ProductBundle' | 'ProductSet', + features: ['Image', 'Quick Add to Cart', 'Wishlist', 'Quick View', 'Rating', 'Sale Badge', 'Stock Status', 'Variant Preview', 'Bundle/Set Indicator'], + lookAndFeel: ['Theme', 'Skeleton', 'Style', 'Hover Effects', 'Responsive'] +} +*/ + +import React, {useState} from 'react' +import PropTypes from 'prop-types' +import {Box, Text, Button, Skeleton, useTheme, Badge, IconButton} from '@salesforce/retail-react-app/app/components/shared/ui' +import {Heart, Eye, ShoppingCart} from '@salesforce/retail-react-app/app/components/shared/icons' +// import ProductImage from './ProductImage' +// import RatingDisplay from './RatingDisplay' +// import VariantPreview from './VariantPreview' + +const ProductTile = ({ + product, + onAddToCart, + onWishlistToggle, + onQuickView, + viewMode = 'grid', + showWishlist = true, + showQuickView = true, + showRating = true +}) => { + const [isLoading, setIsLoading] = useState(false) + const [isHovered, setIsHovered] = useState(false) + const theme = useTheme() + + const handleAddToCart = async () => { + setIsLoading(true) + await onAddToCart(product, 1) + setIsLoading(false) + } + + const getStockStatus = () => { + if (product.stock === 0) return {status: 'out', color: 'red', text: 'Out of Stock'} + if (product.stock <= 5) return {status: 'low', color: 'orange', text: 'Low Stock'} + return {status: 'in', color: 'green', text: 'In Stock'} + } + + const renderBundleOrSetIndicator = () => { + if (product.type === 'bundle' && product.childItems) { + return ( + + Bundle ({product.childItems.length} items) + + ) + } + if (product.type === 'set' && product.setItems) { + return ( + + Set ({product.setItems.length} items) + + ) + } + return null + } + + const stockStatus = getStockStatus() + + return ( + + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + transition="all 0.2s" + _hover={{transform: 'translateY(-2px)', boxShadow: 'lg'}} + cursor="pointer" + > + {renderBundleOrSetIndicator()} + + {/* Product Image */} + + {/* */} + + Product Image + + + {/* Quick Actions on Hover */} + {isHovered && ( + + {showWishlist && ( + } + onClick={(e) => { + e.stopPropagation() + onWishlistToggle?.(product) + }} + aria-label="Add to wishlist" + /> + )} + {showQuickView && ( + } + onClick={(e) => { + e.stopPropagation() + onQuickView?.(product) + }} + aria-label="Quick view" + /> + )} + + )} + + + {/* Product Info */} + + + {product.name} + + + + + ${product.price} + + {product.originalPrice && product.originalPrice > product.price && ( + + ${product.originalPrice} + + )} + + + {/* Rating */} + {showRating && product.rating && ( + + {/* */} + + ★ {product.rating} ({product.reviewCount || 0}) + + + )} + + {/* Stock Status */} + + {stockStatus.text} + + + {/* Quick Add to Cart */} + + + + + ) +} + +ProductTile.propTypes = { + product: PropTypes.object.isRequired, + onAddToCart: PropTypes.func.isRequired, + onWishlistToggle: PropTypes.func, + onQuickView: PropTypes.func, + viewMode: PropTypes.oneOf(['grid', 'list']), + showWishlist: PropTypes.bool, + showQuickView: PropTypes.bool, + showRating: PropTypes.bool +} + +export default ProductTile diff --git a/.cursor/rules/product-component-requirements.mdc b/.cursor/rules/product-component-requirements.mdc new file mode 100644 index 0000000000..b56f5f8e0d --- /dev/null +++ b/.cursor/rules/product-component-requirements.mdc @@ -0,0 +1,33 @@ +--- +description: Requirement template for a Product Detail component (supports single products, bundles, and sets) +globs: +alwaysApply: false +--- + +# Requirement-Driven Product Component Creation (Start Here) + +This rule is your entry point for building Product components in a requirement-driven, modular way. + +## How to Use +- Browse the `.cursor/rules/product-component-requirements-mdc/` directory. +- Each `.mdc` file in that directory describes a specific Product component scenario (e.g., product-tile, product-list, product-detail). +- Each file includes: + - Data model + - Features/props + - Look & feel + - Example requirement object + - Example code usage + - **Best practices** (what to do) + - **Bad practices** (what to avoid) + +## Why This Structure? +- Encourages a requirement-driven, modular approach to component development. +- Makes it easy to find, reuse, and update requirements for common Product scenarios. +- Helps teams avoid common mistakes and maintain high code quality. + +## Adding New Requirements +- Add a new `.mdc` file for each new Product component scenario in the `product-requirements-mdc` directory. +- Include both best and bad practices for maximum clarity. +- Update the directory as new scenarios or conventions arise. + +**Start every Product component with a requirement!** From c7749394a2de22128ba149e4141abf31ab58d58b Mon Sep 17 00:00:00 2001 From: snilakandan Date: Tue, 1 Jul 2025 14:57:53 -0500 Subject: [PATCH 013/626] Rules updated --- .../product-detail-component.mdc | 31 ++++++++++++++++- .../product-list-component.mdc | 33 +++++++++++++++++-- .../product-tile-component.mdc | 3 -- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc index 4a045074fb..49415f5c3b 100644 --- a/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc +++ b/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc @@ -7,7 +7,36 @@ alwaysApply: false # Product Detail Component -## Data Model +## 🚀 Quick Start (Beginner) +If you're new to product components, start here: + +### Basic Product Detail +```jsx +const ProductDetail = ({ product, onAddToCart }) => { + return ( +
+ {product.name} +

{product.name}

+

{product.description}

+

Price: {product.price}

+ +
+ ) +} +``` + +### Essential Props +- `product`: Product data object +- `onAddToCart`: Function to add product to cart +- `className`: Optional styling + +--- + +## 📋 Full Requirements (Advanced) + +### Data Model - Product - Product Bundle (parent with child items; only parent can be added to cart) - Product Set (logical group; all items added to cart together, no parent/child) diff --git a/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc index 9e45a7e2d9..42e4dc11d5 100644 --- a/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc +++ b/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc @@ -1,11 +1,40 @@ --- -description: Product list component requirement template +description: Product list component requirement template globs: "**/components/**" type: agent-requested alwaysApply: false --- # Product List Component +## 🚀 Quick Start (Beginner) +If you're new to product components, start here: + +### Basic Product List +```jsx +const ProductList = ({ products, onAddToCart }) => { + return ( +
+ {products.map(product => ( + + ))} +
+ ) +} +``` + +### Essential Props +- `products`: Array of product objects +- `onAddToCart`: Function to add product to cart +- `className`: Optional styling + +--- + +## 📋 Full Requirements (Advanced) + ## Data Model - Array of Products (single items) - Array of Product Bundles (parent with child items; only parent can be added to cart) @@ -22,7 +51,7 @@ alwaysApply: false - Empty state handling - Product count display - View mode persistence -- Responsive grid/list layouts +- Responsive grid/list layout - Bulk actions (add multiple to cart, wishlist) ## Look & Feel diff --git a/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc index 03fb94e584..07d3b4c847 100644 --- a/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc +++ b/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc @@ -4,9 +4,6 @@ globs: "**/components/**" type: agent-requested alwaysApply: false --- -name: Product Tile Component -description: Requirements and examples for creating product tile components (cards, grid items) -globs: ["**/components/**"] # Product Tile Component From ebdd8a94c4ff2887fed33995637d3a2b8fea8105 Mon Sep 17 00:00:00 2001 From: snilakandan Date: Tue, 1 Jul 2025 15:02:04 -0500 Subject: [PATCH 014/626] Rules updated --- .cursor/rules/product-component-requirements.mdc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.cursor/rules/product-component-requirements.mdc b/.cursor/rules/product-component-requirements.mdc index b56f5f8e0d..1f59dce378 100644 --- a/.cursor/rules/product-component-requirements.mdc +++ b/.cursor/rules/product-component-requirements.mdc @@ -1,6 +1,7 @@ --- -description: Requirement template for a Product Detail component (supports single products, bundles, and sets) -globs: +description: Requirement template for a Product component +globs: "**/components/**" +type: agent-requested alwaysApply: false --- @@ -12,6 +13,7 @@ This rule is your entry point for building Product components in a requirement-d - Browse the `.cursor/rules/product-component-requirements-mdc/` directory. - Each `.mdc` file in that directory describes a specific Product component scenario (e.g., product-tile, product-list, product-detail). - Each file includes: + - Quick Start - Data model - Features/props - Look & feel From 702811bc399b6da48db81125acab680a97d34d68 Mon Sep 17 00:00:00 2001 From: snilakandan Date: Tue, 1 Jul 2025 15:35:08 -0500 Subject: [PATCH 015/626] Updated Readme document --- .../README.md | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/.cursor/rules/product-component-requirements-mdc/README.md b/.cursor/rules/product-component-requirements-mdc/README.md index 67a16e92d1..c14fe6bd3a 100644 --- a/.cursor/rules/product-component-requirements-mdc/README.md +++ b/.cursor/rules/product-component-requirements-mdc/README.md @@ -47,4 +47,37 @@ - Copy the basic examples and modify gradually - Test frequently as you build - Ask for help when stuck - these are complex components! -- Start with mock data before integrating real APIs \ No newline at end of file +- Start with mock data before integrating real APIs + +## 📊 Data Structure: ProductHit + +All product components work with **ProductHit** objects from Salesforce Commerce Cloud's search API: + +```javascript +// Basic ProductHit structure +{ + productId: '25686571M', + productName: 'Product Name', + price: 299.99, + hitType: 'master', // 'master', 'product', 'set', 'bundle' + image: { + alt: 'Product description', + link: 'https://example.com/image.jpg' + } +} +``` + +### Hit Types +- **`master`**: Product with variants (colors, sizes) +- **`product`**: Simple product (single item) +- **`set`**: Product set (multiple items sold together) +- **`bundle`**: Product bundle (parent with child items) + +### Key Properties +- `productId`: Unique identifier +- `productName`: Display name +- `price`: Current price +- `image`: Primary product image +- `imageGroups`: Multiple image sizes +- `variants`: Product variations +- `variationAttributes`: Available options (color, size, etc.) \ No newline at end of file From 17a3b2b00b16ea46d67bef85709c0493f2733985 Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Tue, 1 Jul 2025 17:19:27 -0700 Subject: [PATCH 016/626] Created opentelemetry.js file with utility functions to log OTel spans and metricsas well as creates a test file for it --- packages/pwa-kit-react-sdk/package.json | 7 + .../src/utils/opentelemetry.js | 328 ++++++++++++ .../src/utils/opentelemetry.test.js | 480 ++++++++++++++++++ 3 files changed, 815 insertions(+) create mode 100644 packages/pwa-kit-react-sdk/src/utils/opentelemetry.js create mode 100644 packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js diff --git a/packages/pwa-kit-react-sdk/package.json b/packages/pwa-kit-react-sdk/package.json index 4ec7b7deb0..b01d4b46db 100644 --- a/packages/pwa-kit-react-sdk/package.json +++ b/packages/pwa-kit-react-sdk/package.json @@ -37,6 +37,13 @@ "@loadable/babel-plugin": "^5.15.3", "@loadable/server": "^5.15.3", "@loadable/webpack-plugin": "^5.15.2", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.15.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/utils/opentelemetry.js b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.js new file mode 100644 index 0000000000..db2a0ac9d1 --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.js @@ -0,0 +1,328 @@ +/* + * 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 {trace, context, SpanStatusCode} from '@opentelemetry/api' +import {hrTimeToMilliseconds, hrTimeToTimeStamp} from '@opentelemetry/core' +import logger from './logger-instance' + +const SERVICE_NAME = 'pwa-kit-react-sdk' + +function 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) + + // Create the span data object that matches the expected format + const spanData = { + traceId: spanContext.traceId, + parentId: span.parentSpanId, + name: span.name, + id: spanContext.spanId, + kind: span.kind, + timestamp: hrTimeToTimeStamp(startTime), + duration: duration, + attributes: { + 'service.name': SERVICE_NAME, + ...span.attributes, + event: event // Add event type to distinguish start/end + }, + status: {code: event === 'start' ? SpanStatusCode.UNSET : SpanStatusCode.OK}, + events: [], + links: [], + start_time: startTime, + end_time: endTime, + forwardTrace: process.env.DISABLE_B3_TRACING !== 'true' + } + + // Inject B3 headers into response if available + if (res && process.env.DISABLE_B3_TRACING !== 'true' && event === 'start') { + res.setHeader('x-b3-traceid', spanContext.traceId) + res.setHeader('x-b3-spanid', spanContext.spanId) + res.setHeader('x-b3-sampled', '1') + + if (span.parentSpanId) { + res.setHeader('x-b3-parentspanid', span.parentSpanId) + } + } + + // Only log if this is an end event or if it's a start event for a new span + if (event === 'end' || !span.attributes.hasOwnProperty('event')) { + logger.info('OpenTelemetry span data', { + namespace: 'opentelemetry.logSpanData', + additionalProperties: spanData + }) + } +} + +/** + * Creates a new span with the given name and options + * @param {string} name - The name of the span + * @param {Object} options - Span options + * @returns {Span} The created span + */ +export const createSpan = (name, options = {}) => { + try { + const tracer = trace.getTracer(SERVICE_NAME) + // Get the current context and active span + const ctx = context.active() + const currentSpan = trace.getSpan(ctx) + + // Create a new span with the current context + const span = tracer.startSpan( + name, + { + ...options, + attributes: { + ...options.attributes, + 'service.name': SERVICE_NAME + } + }, + ctx + ) + + // Set the new span as active + logSpanData(span, 'start') + return trace.setSpan(ctx, span) + } catch (error) { + logger.error('Failed to create span', { + namespace: 'opentelemetry', + additionalProperties: { + spanName: name, + error: error.message + } + }) + return null + } +} + +/** + * Creates a child span with the given name and attributes + * @param {string} name - The name of the span + * @param {Object} attributes - The attributes to add to the span + * @returns {Span} The created span + */ +export const createChildSpan = (name, attributes = {}) => { + try { + const tracer = trace.getTracer(SERVICE_NAME) + const ctx = context.active() + const parentSpan = trace.getSpan(ctx) + + // Don't create duplicate spans + if (parentSpan?.attributes?.performance_mark === name) { + return parentSpan + } + + const {performance_mark, performance_detail, ...otherAttributes} = attributes + + const spanAttributes = { + 'service.name': SERVICE_NAME, + ...otherAttributes + } + + if (performance_mark) { + spanAttributes['performance.mark'] = performance_mark + spanAttributes['performance.type'] = 'start' + spanAttributes['performance.detail'] = + typeof performance_detail === 'string' + ? performance_detail + : JSON.stringify(performance_detail) + } + + const span = tracer.startSpan( + name, + { + attributes: spanAttributes + }, + parentSpan ? ctx : undefined + ) + + logSpanData(span, 'start') + return span + } catch (error) { + logger.error('Error creating OpenTelemetry span', { + namespace: 'opentelemetry', + additionalProperties: { + spanName: name, + error: error.message, + stack: error.stack + } + }) + return null + } +} + +/** + * Ends a span and logs its data + * @param {Span} span - The span to end + */ +export const endSpan = (span) => { + if (!span) { + return + } + + try { + const ctx = context.active() + const parentSpan = trace.getSpan(ctx) + + span.end() + + // Log completion data + logSpanData(span, 'end') + } catch (error) { + logger.error('Error ending OpenTelemetry span', { + namespace: 'opentelemetry', + additionalProperties: { + error: error.message, + stack: error.stack + } + }) + } +} + +/** + * Creates a span for performance measurement + * @param {string} name - The name of the performance span + * @param {Function} fn - The function to measure + * @param {Object} res - The response object (optional) + * @returns {Promise} The result of the function + */ +export const tracePerformance = async (name, fn, res = null) => { + const tracer = trace.getTracer(SERVICE_NAME) + // Create the root span + const rootSpan = tracer.startSpan(name, { + attributes: { + 'service.name': SERVICE_NAME + } + }) + + // Create a new context with the root span + const ctx = trace.setSpan(context.active(), rootSpan) + + // Log start event + logSpanData(rootSpan, 'start', res) + + try { + // Run the function within the context of the root span + const result = await context.with(ctx, async () => { + try { + return await fn() + } catch (error) { + rootSpan.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }) + throw error + } + }) + + rootSpan.end() + + // Log completion data + logSpanData(rootSpan, 'end', res) + + return result + } catch (error) { + rootSpan.end() + + // Log error completion + logSpanData(rootSpan, 'end', res) + + throw error + } +} + +/** + * Traces a performance metric + * @param {string} name - The name of the metric + * @param {number} duration - The duration of the metric in milliseconds + * @param {Object} attributes - Additional attributes for the metric + */ +export const logPerformanceMetric = (name, duration, attributes = {}) => { + try { + const tracer = trace.getTracer(SERVICE_NAME) + const ctx = context.active() + const parentSpan = trace.getSpan(ctx) + + if (!parentSpan) { + logger.warn('No parent span found in context', { + namespace: 'opentelemetry', + additionalProperties: {metricName: name} + }) + return + } + + // Extract and normalize performance details + const {performance_mark, performance_detail, ...otherAttributes} = attributes + + // Build metric attributes + const metricAttributes = { + 'service.name': SERVICE_NAME, + 'metric.duration': duration, + ...otherAttributes + } + + if (performance_mark) { + metricAttributes['performance.mark'] = performance_mark + metricAttributes['performance.type'] = 'end' + metricAttributes['performance.detail'] = + typeof performance_detail === 'string' + ? performance_detail + : JSON.stringify(performance_detail) + } + + // Create and immediately end the metric span + const span = tracer.startSpan( + name, + { + attributes: metricAttributes + }, + ctx + ) + + const endTime = hrTimeToTimeStamp(process.hrtime()) + span.end() + + // Log completion data + logSpanData(span, 'end') + } catch (error) { + logger.error('Error logging performance metric', { + namespace: 'opentelemetry', + additionalProperties: { + metricName: name, + error: error.message, + stack: error.stack + } + }) + } +} + +/** + * Traces a performance operation + * @param {string} name - The name of the operation + * @param {Function} fn - The function to trace + * @returns {Promise} The result of the function + */ +export const traceChildPerformance = async (name, fn) => { + const span = createChildSpan(name) + if (!span) { + return fn() + } + + try { + const result = await fn() + endSpan(span) + return result + } catch (error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }) + endSpan(span) + throw error + } +} \ No newline at end of file diff --git a/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js new file mode 100644 index 0000000000..fef33aa569 --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js @@ -0,0 +1,480 @@ +/** + * @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 + */ + +// Mock OpenTelemetry dependencies +jest.mock('@opentelemetry/api', () => ({ + trace: { + getTracer: jest.fn(() => ({ + startSpan: jest.fn() + })), + getSpan: jest.fn(), + setSpan: jest.fn() + }, + context: { + active: jest.fn(), + with: jest.fn() + }, + SpanStatusCode: { + UNSET: 0, + OK: 1, + ERROR: 2 + } +})) + +jest.mock('@opentelemetry/core', () => ({ + hrTimeToMilliseconds: jest.fn(() => 100), + hrTimeToTimeStamp: jest.fn(() => 1234567890) +})) + +jest.mock('./logger-instance', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn() +})) + +describe('OpenTelemetry Utilities', () => { + let mockTracer + let mockSpan + let mockContext + let mockTrace + let mockCore + let mockLogger + let opentelemetryUtils + + beforeEach(() => { + jest.clearAllMocks() + + // Get mocked modules + const api = require('@opentelemetry/api') + const core = require('@opentelemetry/core') + const logger = require('./logger-instance') + + mockTracer = { + startSpan: jest.fn() + } + mockSpan = { + spanContext: jest.fn(() => ({ + traceId: 'test-trace-id', + spanId: 'test-span-id' + })), + parentSpanId: 'test-parent-span-id', + name: 'test-span', + kind: 1, + startTime: [1234567890, 0], + endTime: [1234567890, 100000000], + duration: [0, 100000000], + attributes: {}, + end: jest.fn(), + setStatus: jest.fn() + } + mockContext = { + active: jest.fn(() => 'test-context'), + with: jest.fn() + } + mockTrace = { + getTracer: jest.fn(() => mockTracer), + getSpan: jest.fn(() => mockSpan), + setSpan: jest.fn(() => 'new-context') + } + + // Configure mocks + api.trace = mockTrace + api.context = mockContext + api.SpanStatusCode = { + UNSET: 0, + OK: 1, + ERROR: 2 + } + mockCore = core + mockLogger = logger + + // Configure span mock + mockTracer.startSpan.mockReturnValue(mockSpan) + + // Import the module after mocks are set up + opentelemetryUtils = require('./opentelemetry') + }) + + + + describe('createSpan', () => { + test('should create a span successfully', () => { + const result = opentelemetryUtils.createSpan('test-span', { + attributes: {test: 'value'} + }) + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'test-span', + { + attributes: { + 'service.name': 'pwa-kit-react-sdk', + test: 'value' + } + }, + 'test-context' + ) + expect(mockTrace.setSpan).toHaveBeenCalledWith('test-context', mockSpan) + expect(result).toBe('new-context') + expect(mockLogger.info).toHaveBeenCalledWith( + 'OpenTelemetry span data', + { + namespace: 'opentelemetry.logSpanData', + additionalProperties: expect.objectContaining({ + traceId: 'test-trace-id', + name: 'test-span' + }) + } + ) + }) + + test('should handle errors gracefully', () => { + mockTracer.startSpan.mockImplementation(() => { + throw new Error('Span creation failed') + }) + + const result = opentelemetryUtils.createSpan('test-span') + + expect(result).toBeNull() + expect(mockLogger.error).toHaveBeenCalledWith( + 'Failed to create span', + { + namespace: 'opentelemetry', + additionalProperties: { + spanName: 'test-span', + error: 'Span creation failed' + } + } + ) + }) + }) + + describe('createChildSpan', () => { + test('should create a child span successfully', () => { + const result = opentelemetryUtils.createChildSpan('child-span', { + test: 'value' + }) + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'child-span', + { + attributes: { + 'service.name': 'pwa-kit-react-sdk', + test: 'value' + } + }, + 'test-context' + ) + expect(result).toBe(mockSpan) + // Note: logSpanData may not be called due to the condition in the function + }) + + test('should handle performance mark attributes', () => { + const result = opentelemetryUtils.createChildSpan('perf-span', { + performance_mark: 'test-mark', + performance_detail: 'test-detail', + other: 'value' + }) + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'perf-span', + { + attributes: { + 'service.name': 'pwa-kit-react-sdk', + 'performance.mark': 'test-mark', + 'performance.type': 'start', + 'performance.detail': 'test-detail', + other: 'value' + } + }, + 'test-context' + ) + expect(result).toBe(mockSpan) + }) + + test('should handle errors gracefully', () => { + mockTracer.startSpan.mockImplementation(() => { + throw new Error('Child span creation failed') + }) + + const result = opentelemetryUtils.createChildSpan('child-span') + + expect(result).toBeNull() + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error creating OpenTelemetry span', + { + namespace: 'opentelemetry', + additionalProperties: { + spanName: 'child-span', + error: 'Child span creation failed', + stack: expect.any(String) + } + } + ) + }) + }) + + describe('endSpan', () => { + test('should end a span successfully', () => { + opentelemetryUtils.endSpan(mockSpan) + + expect(mockSpan.end).toHaveBeenCalled() + expect(mockLogger.info).toHaveBeenCalledWith( + 'OpenTelemetry span data', + { + namespace: 'opentelemetry.logSpanData', + additionalProperties: expect.objectContaining({ + traceId: 'test-trace-id', + name: 'test-span' + }) + } + ) + }) + + test('should handle null span gracefully', () => { + opentelemetryUtils.endSpan(null) + + expect(mockSpan.end).not.toHaveBeenCalled() + }) + + test('should handle errors gracefully', () => { + mockSpan.end.mockImplementation(() => { + throw new Error('Span end failed') + }) + + opentelemetryUtils.endSpan(mockSpan) + + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error ending OpenTelemetry span', + { + namespace: 'opentelemetry', + additionalProperties: { + error: 'Span end failed', + stack: expect.any(String) + } + } + ) + }) + }) + + describe('tracePerformance', () => { + test('should trace performance successfully', async () => { + const mockFn = jest.fn().mockResolvedValue('test-result') + const mockRes = { + setHeader: jest.fn() + } + + // Mock context.with to execute the function + mockContext.with.mockImplementation((ctx, fn) => fn()) + + const result = await opentelemetryUtils.tracePerformance('perf-test', mockFn, mockRes) + + expect(mockTracer.startSpan).toHaveBeenCalledWith('perf-test', { + attributes: { + 'service.name': 'pwa-kit-react-sdk' + } + }) + expect(mockContext.with).toHaveBeenCalled() + expect(mockFn).toHaveBeenCalled() + expect(result).toBe('test-result') + expect(mockSpan.end).toHaveBeenCalled() + expect(mockLogger.info).toHaveBeenCalledTimes(2) // start and end + }) + + test('should handle function errors', async () => { + const mockFn = jest.fn().mockRejectedValue(new Error('Function failed')) + const mockRes = { + setHeader: jest.fn() + } + + // Mock context.with to execute the function and throw the error + mockContext.with.mockImplementation((ctx, fn) => fn()) + + await expect(opentelemetryUtils.tracePerformance('perf-test', mockFn, mockRes)) + .rejects.toThrow('Function failed') + + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: 2, // ERROR + message: 'Function failed' + }) + expect(mockSpan.end).toHaveBeenCalled() + }) + + test('should inject B3 headers when tracing is enabled', async () => { + const originalEnv = process.env.DISABLE_B3_TRACING + process.env.DISABLE_B3_TRACING = 'false' + + const mockFn = jest.fn().mockResolvedValue('test-result') + const mockRes = { + setHeader: jest.fn() + } + + // Mock context.with to execute the function + mockContext.with.mockImplementation((ctx, fn) => fn()) + + await opentelemetryUtils.tracePerformance('perf-test', mockFn, mockRes) + + expect(mockRes.setHeader).toHaveBeenCalledWith('x-b3-traceid', 'test-trace-id') + expect(mockRes.setHeader).toHaveBeenCalledWith('x-b3-spanid', 'test-span-id') + expect(mockRes.setHeader).toHaveBeenCalledWith('x-b3-sampled', '1') + expect(mockRes.setHeader).toHaveBeenCalledWith('x-b3-parentspanid', 'test-parent-span-id') + + process.env.DISABLE_B3_TRACING = originalEnv + }) + + test('should not inject B3 headers when tracing is disabled', async () => { + const originalEnv = process.env.DISABLE_B3_TRACING + process.env.DISABLE_B3_TRACING = 'true' + + const mockFn = jest.fn().mockResolvedValue('test-result') + const mockRes = { + setHeader: jest.fn() + } + + // Mock context.with to execute the function + mockContext.with.mockImplementation((ctx, fn) => fn()) + + await opentelemetryUtils.tracePerformance('perf-test', mockFn, mockRes) + + expect(mockRes.setHeader).not.toHaveBeenCalled() + + process.env.DISABLE_B3_TRACING = originalEnv + }) + }) + + describe('logPerformanceMetric', () => { + test('should log performance metric successfully', () => { + opentelemetryUtils.logPerformanceMetric('test-metric', 150, { + test: 'value' + }) + + expect(mockTracer.startSpan).toHaveBeenCalledWith('test-metric', { + attributes: { + 'service.name': 'pwa-kit-react-sdk', + 'metric.duration': 150, + test: 'value' + } + }, 'test-context') + expect(mockSpan.end).toHaveBeenCalled() + expect(mockLogger.info).toHaveBeenCalledWith( + 'OpenTelemetry span data', + { + namespace: 'opentelemetry.logSpanData', + additionalProperties: expect.objectContaining({ + traceId: 'test-trace-id', + name: 'test-span' + }) + } + ) + }) + + test('should handle performance mark attributes', () => { + opentelemetryUtils.logPerformanceMetric('test-metric', 150, { + performance_mark: 'test-mark', + performance_detail: 'test-detail', + other: 'value' + }) + + expect(mockTracer.startSpan).toHaveBeenCalledWith('test-metric', { + attributes: { + 'service.name': 'pwa-kit-react-sdk', + 'metric.duration': 150, + 'performance.mark': 'test-mark', + 'performance.type': 'end', + 'performance.detail': 'test-detail', + other: 'value' + } + }, 'test-context') + }) + + test('should warn when no parent span is found', () => { + mockTrace.getSpan.mockReturnValue(null) + + opentelemetryUtils.logPerformanceMetric('test-metric', 150) + + expect(mockLogger.warn).toHaveBeenCalledWith( + 'No parent span found in context', + { + namespace: 'opentelemetry', + additionalProperties: {metricName: 'test-metric'} + } + ) + expect(mockTracer.startSpan).not.toHaveBeenCalled() + }) + + test('should handle errors gracefully', () => { + // Reset the mock to throw an error + mockTracer.startSpan.mockImplementationOnce(() => { + throw new Error('Metric logging failed') + }) + + opentelemetryUtils.logPerformanceMetric('test-metric', 150) + + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error logging performance metric', + { + namespace: 'opentelemetry', + additionalProperties: { + metricName: 'test-metric', + error: 'Metric logging failed', + stack: expect.any(String) + } + } + ) + }) + }) + + describe('traceChildPerformance', () => { + test('should trace child performance successfully', async () => { + const mockFn = jest.fn().mockResolvedValue('child-result') + + const result = await opentelemetryUtils.traceChildPerformance('child-perf', mockFn) + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'child-perf', + { + attributes: { + 'service.name': 'pwa-kit-react-sdk' + } + }, + 'test-context' + ) + expect(mockFn).toHaveBeenCalled() + expect(result).toBe('child-result') + expect(mockSpan.end).toHaveBeenCalled() + }) + + test('should handle function errors', async () => { + const mockFn = jest.fn().mockRejectedValue(new Error('Child function failed')) + + await expect(opentelemetryUtils.traceChildPerformance('child-perf', mockFn)) + .rejects.toThrow('Child function failed') + + expect(mockSpan.setStatus).toHaveBeenCalledWith({ + code: 2, // ERROR + message: 'Child function failed' + }) + expect(mockSpan.end).toHaveBeenCalled() + }) + + test('should fallback to function execution when span creation fails', async () => { + mockTracer.startSpan.mockImplementation(() => { + throw new Error('Span creation failed') + }) + + const mockFn = jest.fn().mockResolvedValue('fallback-result') + + const result = await opentelemetryUtils.traceChildPerformance('child-perf', mockFn) + + expect(result).toBe('fallback-result') + expect(mockFn).toHaveBeenCalled() + expect(mockSpan.end).not.toHaveBeenCalled() + }) + }) +}) \ No newline at end of file From cae20beaacd8e2d1160a8e5c6c782573992f5c3c Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Tue, 1 Jul 2025 17:27:52 -0700 Subject: [PATCH 017/626] Updated change log file --- packages/pwa-kit-react-sdk/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pwa-kit-react-sdk/CHANGELOG.md b/packages/pwa-kit-react-sdk/CHANGELOG.md index dea62c8181..18f6adfe47 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 opentelemetry.js file with utility functions to log OTel spans and metrics [#2705] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2705) ## 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) From 20eb9b43cadcb2325efbda440c8e22173c0aa1f2 Mon Sep 17 00:00:00 2001 From: Jang ho Jung Date: Tue, 1 Jul 2025 19:33:17 -0700 Subject: [PATCH 018/626] initial file --- packages/pwa-kit-react-sdk/setup-jest.js | 2 +- .../src/utils/opentelemetry.js | 305 ++++++++++++++++++ .../src/utils/performance.js | 112 ++++--- 3 files changed, 380 insertions(+), 39 deletions(-) create mode 100644 packages/pwa-kit-react-sdk/src/utils/opentelemetry.js diff --git a/packages/pwa-kit-react-sdk/setup-jest.js b/packages/pwa-kit-react-sdk/setup-jest.js index 2d94d3e2b1..98410a8d3d 100644 --- a/packages/pwa-kit-react-sdk/setup-jest.js +++ b/packages/pwa-kit-react-sdk/setup-jest.js @@ -40,4 +40,4 @@ 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 +// global.performance = performance diff --git a/packages/pwa-kit-react-sdk/src/utils/opentelemetry.js b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.js new file mode 100644 index 0000000000..ab4433b8a1 --- /dev/null +++ b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.js @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2024, 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 {trace, context, SpanStatusCode} from '@opentelemetry/api' +import {hrTimeToMilliseconds, hrTimeToTimeStamp} from '@opentelemetry/core' +import logger from './logger-instance' + +const SERVICE_NAME = 'pwa-kit-react-sdk' +const tracer = trace.getTracer(SERVICE_NAME) + +function 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) + + // Create the span data object that matches the expected format + const spanData = { + traceId: spanContext.traceId, + parentId: span.parentSpanId, + name: span.name, + id: spanContext.spanId, + kind: span.kind, + timestamp: hrTimeToTimeStamp(startTime), + duration: duration, + attributes: { + 'service.name': SERVICE_NAME, + ...span.attributes, + event: event // Add event type to distinguish start/end + }, + status: {code: event === 'start' ? SpanStatusCode.UNSET : SpanStatusCode.OK}, + events: [], + links: [], + start_time: startTime, + end_time: endTime, + forwardTrace: process.env.DISABLE_B3_TRACING !== 'true' + } + + // Inject B3 headers into response if available + if (res && process.env.DISABLE_B3_TRACING !== 'true' && event === 'start') { + res.setHeader('x-b3-traceid', spanContext.traceId) + res.setHeader('x-b3-spanid', spanContext.spanId) + res.setHeader('x-b3-sampled', '1') + + if (span.parentSpanId) { + res.setHeader('x-b3-parentspanid', span.parentSpanId) + } + } + + // Only log if this is an end event or if it's a start event for a new span + if (event === 'end' || !span.attributes.hasOwnProperty('event')) { + console.info(JSON.stringify(spanData)) + } +} + +/** + * Creates a new span with the given name and options + * @param {string} name - The name of the span + * @param {Object} options - Span options + * @returns {Span} The created span + */ +export const createSpan = (name, options = {}) => { + try { + // Get the current context and active span + const ctx = context.active() + const currentSpan = trace.getSpan(ctx) + + // Create a new span with the current context + const span = tracer.startSpan( + name, + { + ...options, + attributes: { + ...options.attributes, + 'service.name': SERVICE_NAME + } + }, + ctx + ) + + // Set the new span as active + logSpanData(span, 'start') + return trace.setSpan(ctx, span) + } catch (error) { + console.warn(`Failed to create span "${name}":`, error.message) + return null + } +} + +/** + * Creates a child span with the given name and attributes + * @param {string} name - The name of the span + * @param {Object} attributes - The attributes to add to the span + * @returns {Span} The created span + */ +export const createChildSpan = (name, attributes = {}) => { + try { + const ctx = context.active() + const parentSpan = trace.getSpan(ctx) + + // Don't create duplicate spans + if (parentSpan?.attributes?.performance_mark === name) { + return parentSpan + } + + const {performance_mark, performance_detail, ...otherAttributes} = attributes + + const spanAttributes = { + 'service.name': SERVICE_NAME, + ...otherAttributes + } + + if (performance_mark) { + spanAttributes['performance.mark'] = performance_mark + spanAttributes['performance.type'] = 'start' + spanAttributes['performance.detail'] = + typeof performance_detail === 'string' + ? performance_detail + : JSON.stringify(performance_detail) + } + + const span = tracer.startSpan( + name, + { + attributes: spanAttributes + }, + parentSpan ? ctx : undefined + ) + + logSpanData(span, 'start') + return trace.setSpan(ctx, span) + } catch (error) { + logger.error('Error creating OpenTelemetry span', { + namespace: 'opentelemetry', + error: error.message, + stack: error.stack + }) + return null + } +} + +/** + * Ends a span and logs its data + * @param {Span} span - The span to end + */ +export const endSpan = (span) => { + if (!span) { + return + } + + try { + const ctx = context.active() + const parentSpan = trace.getSpan(ctx) + + span.end() + + // Log completion data + logSpanData(span, 'end') + } catch (error) { + logger.error('Error ending OpenTelemetry span', { + namespace: 'opentelemetry', + error: error.message, + stack: error.stack + }) + } +} + +/** + * Creates a span for performance measurement + * @param {string} name - The name of the performance span + * @param {Function} fn - The function to measure + * @param {Object} res - The response object (optional) + * @returns {Promise} The result of the function + */ +export const tracePerformance = async (name, fn, res = null) => { + // Create the root span + const rootSpan = tracer.startSpan(name, { + attributes: { + 'service.name': SERVICE_NAME + } + }) + + // Create a new context with the root span + const ctx = trace.setSpan(context.active(), rootSpan) + + // Log start event + logSpanData(rootSpan, 'start', res) + + try { + // Run the function within the context of the root span + const result = await context.with(ctx, async () => { + try { + return await fn() + } catch (error) { + rootSpan.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }) + throw error + } + }) + + rootSpan.end() + + // Log completion data + logSpanData(rootSpan, 'end', res) + + return result + } catch (error) { + rootSpan.end() + + // Log error completion + logSpanData(rootSpan, 'end', res) + + throw error + } +} + +/** + * Traces a performance metric + * @param {string} name - The name of the metric + * @param {number} duration - The duration of the metric in milliseconds + * @param {Object} attributes - Additional attributes for the metric + */ +export const logPerformanceMetric = (name, duration, attributes = {}) => { + try { + const ctx = context.active() + const parentSpan = trace.getSpan(ctx) + + if (!parentSpan) { + logger.warn('No parent span found in context', {namespace: 'opentelemetry'}) + return + } + + // Extract and normalize performance details + const {performance_mark, performance_detail, ...otherAttributes} = attributes + + // Build metric attributes + const metricAttributes = { + 'service.name': SERVICE_NAME, + 'metric.duration': duration, + ...otherAttributes + } + + if (performance_mark) { + metricAttributes['performance.mark'] = performance_mark + metricAttributes['performance.type'] = 'end' + metricAttributes['performance.detail'] = + typeof performance_detail === 'string' + ? performance_detail + : JSON.stringify(performance_detail) + } + + // Create and immediately end the metric span + const span = tracer.startSpan( + name, + { + attributes: metricAttributes + }, + ctx + ) + + const endTime = hrTimeToTimeStamp(process.hrtime()) + span.end() + + // Log completion data + logSpanData(span, 'end') + } catch (error) { + logger.error('Error logging performance metric', { + namespace: 'opentelemetry', + error: error.message, + stack: error.stack + }) + } +} + +/** + * Traces a performance operation + * @param {string} name - The name of the operation + * @param {Function} fn - The function to trace + * @returns {Promise} The result of the function + */ +export const traceChildPerformance = async (name, fn) => { + const span = createChildSpan(name) + if (!span) { + return fn() + } + + try { + const result = await fn() + endSpan(span) + return result + } catch (error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }) + endSpan(span) + throw error + } +} \ No newline at end of file diff --git a/packages/pwa-kit-react-sdk/src/utils/performance.js b/packages/pwa-kit-react-sdk/src/utils/performance.js index 75e5709d9e..7492191189 100644 --- a/packages/pwa-kit-react-sdk/src/utils/performance.js +++ b/packages/pwa-kit-react-sdk/src/utils/performance.js @@ -5,6 +5,8 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import logger from './logger-instance' +import {createChildSpan, endSpan, logPerformanceMetric} from './opentelemetry' + export const PERFORMANCE_MARKS = { total: 'ssr.total', renderToString: 'ssr.render-to-string', @@ -37,6 +39,7 @@ export default class PerformanceTimer { end: new Map() } this.metrics = [] + this.spans = new Map() } /** @@ -61,17 +64,18 @@ export default class PerformanceTimer { } /** - * A utility function to format and log the performance metrics. - * - * @function - * @private + * Logs all performance metrics */ log() { + // Log each metric once with the standardized format this.metrics.forEach((metric) => { - logger.info(`${metric.name} - ${metric.duration}ms ${metric.detail || ''}`, { - namespace: 'performance' + logPerformanceMetric(metric.name, metric.duration, { + 'performance.detail': metric.detail || '' }) }) + + // Clear the metrics after logging + this.metrics = [] } /** @@ -81,47 +85,79 @@ export default class PerformanceTimer { * @function * @private */ - mark(name, type, options = {}) { - if (!this.enabled) { + mark(name, type, detail = '') { + if (!name || !type || !this.enabled) { return } - if (!name) { - logger.warn('Performance mark cannot be created because the name is undefined.', { - namespace: 'performance' + try { + // Format detail as a string if it's an object + const formattedDetail = typeof detail === 'object' ? JSON.stringify(detail) : detail + + const mark = { + name: `${name}.${type}`, + entryType: 'mark', + startTime: performance.now(), + detail: formattedDetail + } + + performance.mark(mark.name, { + detail: mark.detail }) - return - } - if (type !== this.MARKER_TYPES.START && type !== this.MARKER_TYPES.END) { - logger.warn( - 'Performance mark cannot be created because the type must be either "start" or "end".', - { - namespace: 'performance' + // Only create spans for 'start' events and store them for later use + if (type === 'start') { + if (!this.spans.has(name)) { + const span = createChildSpan(name, { + performance_mark: name, + performance_type: type, + performance_detail: formattedDetail + }) + if (span) { + this.spans.set(name, span) + } } - ) - return - } + } else if (type === 'end') { + const startMark = `${name}.start` + const endMark = `${name}.end` - const timestamp = performance.now() - const isEnd = type === this.MARKER_TYPES.END - const storage = isEnd ? this.marks.end : this.marks.start - storage.set(name, { - name, - timestamp, - detail: options.detail - }) + try { + const measure = performance.measure(name, startMark, endMark) - if (isEnd) { - const startMark = this.marks.start.get(name) - if (startMark) { - const measurement = { - name, - duration: timestamp - startMark.timestamp, - detail: options.detail + // Add the metric to the metrics array for Server-Timing header + this.metrics.push({ + name, + duration: measure.duration, + detail: formattedDetail + }) + + // End the corresponding span if it exists + const span = this.spans.get(name) + if (span) { + endSpan(span) + this.spans.delete(name) + } + + // Clear the marks + performance.clearMarks(startMark) + performance.clearMarks(endMark) + performance.clearMeasures(name) + } catch (error) { + logger.warn('Failed to measure performance mark', { + name, + error: error.message, + startMark, + endMark + }) } - this.metrics.push(measurement) } + } catch (error) { + logger.error('Error creating performance mark', { + name, + type, + error: error.message, + stack: error.stack + }) } } -} +} \ No newline at end of file From abbcf6d2ef18b879fc39309cfa7fcb0e13ede057 Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Wed, 2 Jul 2025 09:33:42 -0700 Subject: [PATCH 019/626] Linted utility and test files --- .../src/utils/opentelemetry.js | 9 +- .../src/utils/opentelemetry.test.js | 202 +++++++++--------- 2 files changed, 102 insertions(+), 109 deletions(-) diff --git a/packages/pwa-kit-react-sdk/src/utils/opentelemetry.js b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.js index db2a0ac9d1..4837fb89b1 100644 --- a/packages/pwa-kit-react-sdk/src/utils/opentelemetry.js +++ b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.js @@ -51,7 +51,7 @@ function logSpanData(span, event = 'start', res = null) { } // Only log if this is an end event or if it's a start event for a new span - if (event === 'end' || !span.attributes.hasOwnProperty('event')) { + if (event === 'end' || !Object.prototype.hasOwnProperty.call(span.attributes, 'event')) { logger.info('OpenTelemetry span data', { namespace: 'opentelemetry.logSpanData', additionalProperties: spanData @@ -70,7 +70,7 @@ export const createSpan = (name, options = {}) => { const tracer = trace.getTracer(SERVICE_NAME) // Get the current context and active span const ctx = context.active() - const currentSpan = trace.getSpan(ctx) + // Note: currentSpan is not used in this implementation // Create a new span with the current context const span = tracer.startSpan( @@ -167,7 +167,7 @@ export const endSpan = (span) => { try { const ctx = context.active() - const parentSpan = trace.getSpan(ctx) + // Note: parentSpan is not used in this implementation span.end() @@ -284,7 +284,6 @@ export const logPerformanceMetric = (name, duration, attributes = {}) => { ctx ) - const endTime = hrTimeToTimeStamp(process.hrtime()) span.end() // Log completion data @@ -325,4 +324,4 @@ export const traceChildPerformance = async (name, fn) => { endSpan(span) throw error } -} \ No newline at end of file +} diff --git a/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js index fef33aa569..c6a2e848d9 100644 --- a/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js +++ b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js @@ -1,5 +1,8 @@ -/** - * @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 */ /* * Copyright (c) 2025, Salesforce, Inc. @@ -52,10 +55,13 @@ describe('OpenTelemetry Utilities', () => { jest.clearAllMocks() // Get mocked modules + // eslint-disable-next-line @typescript-eslint/no-var-requires const api = require('@opentelemetry/api') + // eslint-disable-next-line @typescript-eslint/no-var-requires const core = require('@opentelemetry/core') + // eslint-disable-next-line @typescript-eslint/no-var-requires const logger = require('./logger-instance') - + mockTracer = { startSpan: jest.fn() } @@ -92,6 +98,7 @@ describe('OpenTelemetry Utilities', () => { OK: 1, ERROR: 2 } + mockCore = core mockLogger = logger @@ -102,8 +109,6 @@ describe('OpenTelemetry Utilities', () => { opentelemetryUtils = require('./opentelemetry') }) - - describe('createSpan', () => { test('should create a span successfully', () => { const result = opentelemetryUtils.createSpan('test-span', { @@ -122,16 +127,13 @@ describe('OpenTelemetry Utilities', () => { ) expect(mockTrace.setSpan).toHaveBeenCalledWith('test-context', mockSpan) expect(result).toBe('new-context') - expect(mockLogger.info).toHaveBeenCalledWith( - 'OpenTelemetry span data', - { - namespace: 'opentelemetry.logSpanData', - additionalProperties: expect.objectContaining({ - traceId: 'test-trace-id', - name: 'test-span' - }) - } - ) + expect(mockLogger.info).toHaveBeenCalledWith('OpenTelemetry span data', { + namespace: 'opentelemetry.logSpanData', + additionalProperties: expect.objectContaining({ + traceId: 'test-trace-id', + name: 'test-span' + }) + }) }) test('should handle errors gracefully', () => { @@ -142,16 +144,13 @@ describe('OpenTelemetry Utilities', () => { const result = opentelemetryUtils.createSpan('test-span') expect(result).toBeNull() - expect(mockLogger.error).toHaveBeenCalledWith( - 'Failed to create span', - { - namespace: 'opentelemetry', - additionalProperties: { - spanName: 'test-span', - error: 'Span creation failed' - } + expect(mockLogger.error).toHaveBeenCalledWith('Failed to create span', { + namespace: 'opentelemetry', + additionalProperties: { + spanName: 'test-span', + error: 'Span creation failed' } - ) + }) }) }) @@ -206,17 +205,14 @@ describe('OpenTelemetry Utilities', () => { const result = opentelemetryUtils.createChildSpan('child-span') expect(result).toBeNull() - expect(mockLogger.error).toHaveBeenCalledWith( - 'Error creating OpenTelemetry span', - { - namespace: 'opentelemetry', - additionalProperties: { - spanName: 'child-span', - error: 'Child span creation failed', - stack: expect.any(String) - } + expect(mockLogger.error).toHaveBeenCalledWith('Error creating OpenTelemetry span', { + namespace: 'opentelemetry', + additionalProperties: { + spanName: 'child-span', + error: 'Child span creation failed', + stack: expect.any(String) } - ) + }) }) }) @@ -225,16 +221,13 @@ describe('OpenTelemetry Utilities', () => { opentelemetryUtils.endSpan(mockSpan) expect(mockSpan.end).toHaveBeenCalled() - expect(mockLogger.info).toHaveBeenCalledWith( - 'OpenTelemetry span data', - { - namespace: 'opentelemetry.logSpanData', - additionalProperties: expect.objectContaining({ - traceId: 'test-trace-id', - name: 'test-span' - }) - } - ) + expect(mockLogger.info).toHaveBeenCalledWith('OpenTelemetry span data', { + namespace: 'opentelemetry.logSpanData', + additionalProperties: expect.objectContaining({ + traceId: 'test-trace-id', + name: 'test-span' + }) + }) }) test('should handle null span gracefully', () => { @@ -250,16 +243,13 @@ describe('OpenTelemetry Utilities', () => { opentelemetryUtils.endSpan(mockSpan) - expect(mockLogger.error).toHaveBeenCalledWith( - 'Error ending OpenTelemetry span', - { - namespace: 'opentelemetry', - additionalProperties: { - error: 'Span end failed', - stack: expect.any(String) - } + expect(mockLogger.error).toHaveBeenCalledWith('Error ending OpenTelemetry span', { + namespace: 'opentelemetry', + additionalProperties: { + error: 'Span end failed', + stack: expect.any(String) } - ) + }) }) }) @@ -296,8 +286,9 @@ describe('OpenTelemetry Utilities', () => { // Mock context.with to execute the function and throw the error mockContext.with.mockImplementation((ctx, fn) => fn()) - await expect(opentelemetryUtils.tracePerformance('perf-test', mockFn, mockRes)) - .rejects.toThrow('Function failed') + await expect( + opentelemetryUtils.tracePerformance('perf-test', mockFn, mockRes) + ).rejects.toThrow('Function failed') expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: 2, // ERROR @@ -309,7 +300,7 @@ describe('OpenTelemetry Utilities', () => { test('should inject B3 headers when tracing is enabled', async () => { const originalEnv = process.env.DISABLE_B3_TRACING process.env.DISABLE_B3_TRACING = 'false' - + const mockFn = jest.fn().mockResolvedValue('test-result') const mockRes = { setHeader: jest.fn() @@ -323,7 +314,10 @@ describe('OpenTelemetry Utilities', () => { expect(mockRes.setHeader).toHaveBeenCalledWith('x-b3-traceid', 'test-trace-id') expect(mockRes.setHeader).toHaveBeenCalledWith('x-b3-spanid', 'test-span-id') expect(mockRes.setHeader).toHaveBeenCalledWith('x-b3-sampled', '1') - expect(mockRes.setHeader).toHaveBeenCalledWith('x-b3-parentspanid', 'test-parent-span-id') + expect(mockRes.setHeader).toHaveBeenCalledWith( + 'x-b3-parentspanid', + 'test-parent-span-id' + ) process.env.DISABLE_B3_TRACING = originalEnv }) @@ -331,7 +325,7 @@ describe('OpenTelemetry Utilities', () => { test('should not inject B3 headers when tracing is disabled', async () => { const originalEnv = process.env.DISABLE_B3_TRACING process.env.DISABLE_B3_TRACING = 'true' - + const mockFn = jest.fn().mockResolvedValue('test-result') const mockRes = { setHeader: jest.fn() @@ -354,24 +348,25 @@ describe('OpenTelemetry Utilities', () => { test: 'value' }) - expect(mockTracer.startSpan).toHaveBeenCalledWith('test-metric', { - attributes: { - 'service.name': 'pwa-kit-react-sdk', - 'metric.duration': 150, - test: 'value' - } - }, 'test-context') - expect(mockSpan.end).toHaveBeenCalled() - expect(mockLogger.info).toHaveBeenCalledWith( - 'OpenTelemetry span data', + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'test-metric', { - namespace: 'opentelemetry.logSpanData', - additionalProperties: expect.objectContaining({ - traceId: 'test-trace-id', - name: 'test-span' - }) - } + attributes: { + 'service.name': 'pwa-kit-react-sdk', + 'metric.duration': 150, + test: 'value' + } + }, + 'test-context' ) + expect(mockSpan.end).toHaveBeenCalled() + expect(mockLogger.info).toHaveBeenCalledWith('OpenTelemetry span data', { + namespace: 'opentelemetry.logSpanData', + additionalProperties: expect.objectContaining({ + traceId: 'test-trace-id', + name: 'test-span' + }) + }) }) test('should handle performance mark attributes', () => { @@ -381,16 +376,20 @@ describe('OpenTelemetry Utilities', () => { other: 'value' }) - expect(mockTracer.startSpan).toHaveBeenCalledWith('test-metric', { - attributes: { - 'service.name': 'pwa-kit-react-sdk', - 'metric.duration': 150, - 'performance.mark': 'test-mark', - 'performance.type': 'end', - 'performance.detail': 'test-detail', - other: 'value' - } - }, 'test-context') + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'test-metric', + { + attributes: { + 'service.name': 'pwa-kit-react-sdk', + 'metric.duration': 150, + 'performance.mark': 'test-mark', + 'performance.type': 'end', + 'performance.detail': 'test-detail', + other: 'value' + } + }, + 'test-context' + ) }) test('should warn when no parent span is found', () => { @@ -398,13 +397,10 @@ describe('OpenTelemetry Utilities', () => { opentelemetryUtils.logPerformanceMetric('test-metric', 150) - expect(mockLogger.warn).toHaveBeenCalledWith( - 'No parent span found in context', - { - namespace: 'opentelemetry', - additionalProperties: {metricName: 'test-metric'} - } - ) + expect(mockLogger.warn).toHaveBeenCalledWith('No parent span found in context', { + namespace: 'opentelemetry', + additionalProperties: {metricName: 'test-metric'} + }) expect(mockTracer.startSpan).not.toHaveBeenCalled() }) @@ -416,17 +412,14 @@ describe('OpenTelemetry Utilities', () => { opentelemetryUtils.logPerformanceMetric('test-metric', 150) - expect(mockLogger.error).toHaveBeenCalledWith( - 'Error logging performance metric', - { - namespace: 'opentelemetry', - additionalProperties: { - metricName: 'test-metric', - error: 'Metric logging failed', - stack: expect.any(String) - } + expect(mockLogger.error).toHaveBeenCalledWith('Error logging performance metric', { + namespace: 'opentelemetry', + additionalProperties: { + metricName: 'test-metric', + error: 'Metric logging failed', + stack: expect.any(String) } - ) + }) }) }) @@ -453,8 +446,9 @@ describe('OpenTelemetry Utilities', () => { test('should handle function errors', async () => { const mockFn = jest.fn().mockRejectedValue(new Error('Child function failed')) - await expect(opentelemetryUtils.traceChildPerformance('child-perf', mockFn)) - .rejects.toThrow('Child function failed') + await expect( + opentelemetryUtils.traceChildPerformance('child-perf', mockFn) + ).rejects.toThrow('Child function failed') expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: 2, // ERROR @@ -477,4 +471,4 @@ describe('OpenTelemetry Utilities', () => { expect(mockSpan.end).not.toHaveBeenCalled() }) }) -}) \ No newline at end of file +}) From b299d4e44ef1c36f27b89e7995f36a04291a9c1b Mon Sep 17 00:00:00 2001 From: snilakandan Date: Wed, 2 Jul 2025 11:53:21 -0500 Subject: [PATCH 020/626] Updated rules --- .../README.md | 94 ++---- .../product-detail-component.mdc | 96 +----- .../product-list-component.mdc | 314 +----------------- .../product-tile-component.mdc | 197 +---------- .../rules/product-component-requirements.mdc | 1 - 5 files changed, 28 insertions(+), 674 deletions(-) diff --git a/.cursor/rules/product-component-requirements-mdc/README.md b/.cursor/rules/product-component-requirements-mdc/README.md index c14fe6bd3a..490e4ac5a6 100644 --- a/.cursor/rules/product-component-requirements-mdc/README.md +++ b/.cursor/rules/product-component-requirements-mdc/README.md @@ -1,83 +1,27 @@ -# Product Component Development Guide +# Product Component Requirements -## 🚀 Getting Started (Beginner-Friendly) +This directory contains hybrid rules for product components that combine structured requirements with intelligent analysis. Each rule provides clear guidance on what to build while instructing the AI to study the actual implementation in `node_modules/@salesforce/retail-react-app`. -### Quick Start Path -1. **Start Simple**: Use `product-tile-component.mdc` for basic product displays -2. **Add Lists**: Move to `product-list-component.mdc` when you need collections -3. **Go Deep**: Use `product-detail-component.mdc` for full product pages +## Rules -### For New Developers -- Focus on one component type at a time -- Start with the "Basic Implementation" examples -- Skip advanced features until you're comfortable with basics -- Use the "Common Mistakes" sections to avoid pitfalls +- `product-detail-component.mdc` - Requirements and analysis for product detail pages +- `product-list-component.mdc` - Requirements and analysis for product listing pages +- `product-tile-component.mdc` - Requirements and analysis for product tile components -### When You're Ready for More -- Explore "Advanced Features" sections -- Study the "Best Practices" examples -- Review "Testing Guidelines" for quality assurance +## Hybrid Approach -## 📚 Component Complexity Levels +Each rule combines: -| Component | Complexity | Best For | -|-----------|------------|----------| -| Product Tile | ⭐ Beginner | Simple product cards, grids | -| Product List | ⭐⭐ Intermediate | Product collections, search results | -| Product Detail | ⭐⭐⭐ Advanced | Full product pages, complex layouts | +### 📋 **Structured Requirements** +- Clear data models (Product, Bundle, Set) +- Essential features and props +- Best practices and bad practices +- Look & feel requirements -## 🎯 Quick Reference +### 🔍 **Analysis-Based Guidance** +- Instructions to analyze actual codebase +- Study real implementation patterns +- Guide developers based on actual code +- Provide contextual advice -### Product Tile -- **Use when**: Displaying products in grids, carousels, or lists -- **Key features**: Image, name, price, quick actions -- **Start here**: Perfect for beginners - -### Product List -- **Use when**: Showing multiple products with filtering/sorting -- **Key features**: Grid/list views, pagination, filters -- **Prerequisites**: Comfortable with Product Tiles - -### Product Detail -- **Use when**: Full product information pages -- **Key features**: Complete product data, variants, add to cart -- **Prerequisites**: Experience with Tiles and Lists - -## 💡 Tips for Success -- Copy the basic examples and modify gradually -- Test frequently as you build -- Ask for help when stuck - these are complex components! -- Start with mock data before integrating real APIs - -## 📊 Data Structure: ProductHit - -All product components work with **ProductHit** objects from Salesforce Commerce Cloud's search API: - -```javascript -// Basic ProductHit structure -{ - productId: '25686571M', - productName: 'Product Name', - price: 299.99, - hitType: 'master', // 'master', 'product', 'set', 'bundle' - image: { - alt: 'Product description', - link: 'https://example.com/image.jpg' - } -} -``` - -### Hit Types -- **`master`**: Product with variants (colors, sizes) -- **`product`**: Simple product (single item) -- **`set`**: Product set (multiple items sold together) -- **`bundle`**: Product bundle (parent with child items) - -### Key Properties -- `productId`: Unique identifier -- `productName`: Display name -- `price`: Current price -- `image`: Primary product image -- `imageGroups`: Multiple image sizes -- `variants`: Product variations -- `variationAttributes`: Available options (color, size, etc.) \ No newline at end of file +This approach provides the clarity of structured requirements while leveraging the intelligence of analysis-based guidance. \ No newline at end of file diff --git a/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc index 49415f5c3b..889bde948f 100644 --- a/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc +++ b/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc @@ -1,7 +1,6 @@ --- description: Product Detail component requirement template globs: "**/components/**" -type: agent-requested alwaysApply: false --- @@ -90,98 +89,9 @@ const requirement = { - Skipping accessibility (missing alt text, no keyboard support). - Not validating props or documenting requirements. -## Example Component Skeleton -```jsx -/* -Requirement: -{ - dataModel: 'Product' | 'ProductBundle' | 'ProductSet', - features: ['Gallery', 'Add to Cart', 'Quantity Selector', 'Show Reviews', 'Variant Selection', 'Show Bundle/Set Items'], - lookAndFeel: ['Theme', 'Skeleton', 'Style'] -} -*/ - -import React, {useState} from 'react' -import PropTypes from 'prop-types' -import {Box, Text, Button, Skeleton, useTheme} from '@salesforce/retail-react-app/app/components/shared/ui' -import {QuantityPicker} from '@salesforce/retail-react-app/app/components/quantity-picker' -import RecommendedProducts from '@salesforce/retail-react-app/app/components/recommended-products' -// import ProductGallery from './ProductGallery' -// import ProductReviews from './ProductReviews' -// import VariantSelector from './VariantSelector' - -const ProductDetail = ({product, onAddToCart, showReviews = true, showRecommendations = true}) => { - const [quantity, setQuantity] = useState(1) - const [isLoading, setIsLoading] = useState(false) - const theme = useTheme() - - const handleAddToCart = async () => { - setIsLoading(true) - await onAddToCart(product, quantity) - setIsLoading(false) - } - - // Example: Render bundle/set items - const renderBundleOrSetItems = () => { - if (product.type === 'bundle' && product.childItems) { - return ( - - Bundle Includes: - {product.childItems.map((item) => ( - - {item.name} - {/* No add to cart for child items */} - - ))} - - ) - } - if (product.type === 'set' && product.setItems) { - return ( - - Set Includes: - {product.setItems.map((item) => ( - - {item.name} - {/* All items in set are added to cart together */} - - ))} - - ) - } - return null - } +--- - return ( - - - {/* */} - {product.name} - {product.description} - {/* */} - - - {renderBundleOrSetItems()} - {showReviews && ( - // - Reviews go here - )} - {showRecommendations && ( - - )} - - - ) -} +## 🔍 Analysis Rule -ProductDetail.propTypes = { - product: PropTypes.object.isRequired, - onAddToCart: PropTypes.func.isRequired, - showReviews: PropTypes.bool, - showRecommendations: PropTypes.bool -} +Analyze node_modules/@salesforce/retail-react-app/app/pages/product-detail/ to understand how product detail pages are implemented. Study the data models, component structure, and patterns used for single products, bundles, and sets. Guide developers based on actual implementation patterns found in the template. -export default ProductDetail -``` diff --git a/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc index 42e4dc11d5..3a35c60694 100644 --- a/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc +++ b/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc @@ -1,7 +1,6 @@ --- description: Product list component requirement template globs: "**/components/**" -type: agent-requested alwaysApply: false --- # Product List Component @@ -113,315 +112,8 @@ const requirement = { - Loading all products at once (no virtualization for large lists). - Not validating props or documenting requirements. -## Example Component Skeleton -```jsx -/* -Requirement: -{ - dataModel: 'Product[]' | 'ProductBundle[]' | 'ProductSet[]' | 'Mixed[]', - features: ['Grid/List Toggle', 'Sorting', 'Filtering', 'Pagination', 'Search', 'Loading States', 'Empty State', 'Product Count', 'View Persistence', 'Responsive Layout', 'Bulk Actions'], - lookAndFeel: ['Theme', 'Skeleton', 'Style', 'Responsive', 'Loading Animations', 'Empty State Design'] -} -*/ - -import React, {useState, useEffect, useMemo} from 'react' -import PropTypes from 'prop-types' -import {Box, Text, Button, Skeleton, useTheme, Select, HStack, VStack, Grid, Flex, Spinner} from '@salesforce/retail-react-app/app/components/shared/ui' -import {Grid as GridIcon, List as ListIcon, Filter, Search} from '@salesforce/retail-react-app/app/components/shared/icons' -import ProductTile from './ProductTile' -// import ProductListTile from './ProductListTile' -// import ProductFilters from './ProductFilters' -// import ProductSearch from './ProductSearch' - -const ProductList = ({ - products = [], - onAddToCart, - onWishlistToggle, - onQuickView, - viewMode: initialViewMode = 'grid', - sortBy: initialSortBy = 'name', - filters = {}, - onLoadMore, - isLoading = false, - hasMore = false, - showFilters = true, - showSearch = true, - showSort = true, - showViewToggle = true, - itemsPerPage = 12 -}) => { - const [viewMode, setViewMode] = useState(initialViewMode) - const [sortBy, setSortBy] = useState(initialSortBy) - const [currentFilters, setCurrentFilters] = useState(filters) - const [searchQuery, setSearchQuery] = useState('') - const [currentPage, setCurrentPage] = useState(1) - const theme = useTheme() - - // Persist view mode preference - useEffect(() => { - localStorage.setItem('productListViewMode', viewMode) - }, [viewMode]) - - // Load view mode preference - useEffect(() => { - const savedViewMode = localStorage.getItem('productListViewMode') - if (savedViewMode) setViewMode(savedViewMode) - }, []) - - // Sort and filter products - const processedProducts = useMemo(() => { - let filtered = products.filter(product => { - // Search filter - if (searchQuery && !product.name.toLowerCase().includes(searchQuery.toLowerCase())) { - return false - } - - // Apply other filters (category, price range, etc.) - if (currentFilters.category && product.category !== currentFilters.category) { - return false - } - if (currentFilters.minPrice && product.price < currentFilters.minPrice) { - return false - } - if (currentFilters.maxPrice && product.price > currentFilters.maxPrice) { - return false - } - if (currentFilters.inStock && product.stock === 0) { - return false - } - - return true - }) - - // Sort products - filtered.sort((a, b) => { - switch (sortBy) { - case 'price': - return a.price - b.price - case 'price-desc': - return b.price - a.price - case 'name': - return a.name.localeCompare(b.name) - case 'rating': - return (b.rating || 0) - (a.rating || 0) - case 'newest': - return new Date(b.createdAt) - new Date(a.createdAt) - case 'popularity': - return (b.salesCount || 0) - (a.salesCount || 0) - default: - return 0 - } - }) - - return filtered - }, [products, searchQuery, currentFilters, sortBy]) - - // Paginate products - const paginatedProducts = useMemo(() => { - const startIndex = (currentPage - 1) * itemsPerPage - return processedProducts.slice(startIndex, startIndex + itemsPerPage) - }, [processedProducts, currentPage, itemsPerPage]) - - const handleLoadMore = () => { - if (hasMore && onLoadMore) { - onLoadMore() - } else { - setCurrentPage(prev => prev + 1) - } - } - - const handleFilterChange = (newFilters) => { - setCurrentFilters(newFilters) - setCurrentPage(1) // Reset to first page when filters change - } - - const renderEmptyState = () => ( - - - No products found - - - Try adjusting your search or filters to find what you're looking for. - - - - ) - - const renderProductGrid = () => ( - - {paginatedProducts.map((product) => ( - - ))} - - ) - - const renderProductList = () => ( - - {paginatedProducts.map((product) => ( - // - - {product.name} - ${product.price} - - ))} - - ) - - return ( - - {/* Header with controls */} - - - - Products ({processedProducts.length}) - - - - {/* Search */} - {showSearch && ( - - - setSearchQuery(e.target.value)} - style={{ - padding: '8px 12px 8px 36px', - border: '1px solid #e2e8f0', - borderRadius: '6px', - width: '250px' - }} - /> - - )} - - {/* Sort */} - {showSort && ( - - )} - - {/* View Toggle */} - {showViewToggle && ( - - - - - )} - - - - {/* Filters */} - {showFilters && ( - - {/* */} - - Filters: {Object.keys(currentFilters).length > 0 ? - Object.entries(currentFilters).map(([key, value]) => `${key}: ${value}`).join(', ') : - 'None applied' - } - - - )} - - - {/* Product Grid/List */} - - {processedProducts.length === 0 ? ( - renderEmptyState() - ) : ( - <> - {viewMode === 'grid' ? renderProductGrid() : renderProductList()} - - {/* Load More / Pagination */} - {(hasMore || currentPage * itemsPerPage < processedProducts.length) && ( - - - - )} - - )} - - - ) -} +--- -ProductList.propTypes = { - products: PropTypes.array.isRequired, - onAddToCart: PropTypes.func.isRequired, - onWishlistToggle: PropTypes.func, - onQuickView: PropTypes.func, - viewMode: PropTypes.oneOf(['grid', 'list']), - sortBy: PropTypes.string, - filters: PropTypes.object, - onLoadMore: PropTypes.func, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - showFilters: PropTypes.bool, - showSearch: PropTypes.bool, - showSort: PropTypes.bool, - showViewToggle: PropTypes.bool, - itemsPerPage: PropTypes.number -} +## 🔍 Analysis Rule -export default ProductList +Analyze node_modules/@salesforce/retail-react-app/app/pages/product-list/ to understand how product listing pages are implemented. Study the search functionality, filtering, pagination, and grid/list display patterns. diff --git a/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc index 07d3b4c847..c66cbf6e62 100644 --- a/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc +++ b/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc @@ -1,7 +1,6 @@ --- description: Product Tile component requirement template globs: "**/components/**" -type: agent-requested alwaysApply: false --- @@ -102,199 +101,9 @@ const requirement = { - Ignoring accessibility (no keyboard support, missing ARIA labels). - Not validating props or documenting requirements. -## Example Component Skeleton -```jsx -/* -Requirement: -{ - dataModel: 'Product' | 'ProductBundle' | 'ProductSet', - features: ['Image', 'Quick Add to Cart', 'Wishlist', 'Quick View', 'Rating', 'Sale Badge', 'Stock Status', 'Variant Preview', 'Bundle/Set Indicator'], - lookAndFeel: ['Theme', 'Skeleton', 'Style', 'Hover Effects', 'Responsive'] -} -*/ - -import React, {useState} from 'react' -import PropTypes from 'prop-types' -import {Box, Text, Button, Skeleton, useTheme, Badge, IconButton} from '@salesforce/retail-react-app/app/components/shared/ui' -import {Heart, Eye, ShoppingCart} from '@salesforce/retail-react-app/app/components/shared/icons' -// import ProductImage from './ProductImage' -// import RatingDisplay from './RatingDisplay' -// import VariantPreview from './VariantPreview' - -const ProductTile = ({ - product, - onAddToCart, - onWishlistToggle, - onQuickView, - viewMode = 'grid', - showWishlist = true, - showQuickView = true, - showRating = true -}) => { - const [isLoading, setIsLoading] = useState(false) - const [isHovered, setIsHovered] = useState(false) - const theme = useTheme() - - const handleAddToCart = async () => { - setIsLoading(true) - await onAddToCart(product, 1) - setIsLoading(false) - } - - const getStockStatus = () => { - if (product.stock === 0) return {status: 'out', color: 'red', text: 'Out of Stock'} - if (product.stock <= 5) return {status: 'low', color: 'orange', text: 'Low Stock'} - return {status: 'in', color: 'green', text: 'In Stock'} - } - - const renderBundleOrSetIndicator = () => { - if (product.type === 'bundle' && product.childItems) { - return ( - - Bundle ({product.childItems.length} items) - - ) - } - if (product.type === 'set' && product.setItems) { - return ( - - Set ({product.setItems.length} items) - - ) - } - return null - } - - const stockStatus = getStockStatus() - - return ( - - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - transition="all 0.2s" - _hover={{transform: 'translateY(-2px)', boxShadow: 'lg'}} - cursor="pointer" - > - {renderBundleOrSetIndicator()} - - {/* Product Image */} - - {/* */} - - Product Image - - - {/* Quick Actions on Hover */} - {isHovered && ( - - {showWishlist && ( - } - onClick={(e) => { - e.stopPropagation() - onWishlistToggle?.(product) - }} - aria-label="Add to wishlist" - /> - )} - {showQuickView && ( - } - onClick={(e) => { - e.stopPropagation() - onQuickView?.(product) - }} - aria-label="Quick view" - /> - )} - - )} - - - {/* Product Info */} - - - {product.name} - - - - - ${product.price} - - {product.originalPrice && product.originalPrice > product.price && ( - - ${product.originalPrice} - - )} - - - {/* Rating */} - {showRating && product.rating && ( - - {/* */} - - ★ {product.rating} ({product.reviewCount || 0}) - - - )} +--- - {/* Stock Status */} - - {stockStatus.text} - +## 🔍 Analysis Rule - {/* Quick Add to Cart */} - - - - - ) -} - -ProductTile.propTypes = { - product: PropTypes.object.isRequired, - onAddToCart: PropTypes.func.isRequired, - onWishlistToggle: PropTypes.func, - onQuickView: PropTypes.func, - viewMode: PropTypes.oneOf(['grid', 'list']), - showWishlist: PropTypes.bool, - showQuickView: PropTypes.bool, - showRating: PropTypes.bool -} +Analyze node_modules/@salesforce/retail-react-app/app/components/product-tile/ to understand how product tiles are implemented. Study the image handling, pricing display, variant selection, and interaction patterns. -export default ProductTile diff --git a/.cursor/rules/product-component-requirements.mdc b/.cursor/rules/product-component-requirements.mdc index 1f59dce378..8864e22e52 100644 --- a/.cursor/rules/product-component-requirements.mdc +++ b/.cursor/rules/product-component-requirements.mdc @@ -1,7 +1,6 @@ --- description: Requirement template for a Product component globs: "**/components/**" -type: agent-requested alwaysApply: false --- From a2bff8baa4a2198e84e3acd3bf4b7f36be973c99 Mon Sep 17 00:00:00 2001 From: snilakandan Date: Wed, 2 Jul 2025 14:13:47 -0500 Subject: [PATCH 021/626] updated rules --- .../product-detail-component.mdc | 2 +- .../product-list-component.mdc | 2 +- .../product-tile-component.mdc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc index 889bde948f..c6eb870770 100644 --- a/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc +++ b/.cursor/rules/product-component-requirements-mdc/product-detail-component.mdc @@ -93,5 +93,5 @@ const requirement = { ## 🔍 Analysis Rule -Analyze node_modules/@salesforce/retail-react-app/app/pages/product-detail/ to understand how product detail pages are implemented. Study the data models, component structure, and patterns used for single products, bundles, and sets. Guide developers based on actual implementation patterns found in the template. +Analyze "node_modules/@salesforce/*retail-react-app*/app/pages/*product*/" to understand how product detail pages are implemented. Study the data models, component structure, and patterns used for single products, bundles, and sets. Guide developers based on actual implementation patterns found in the template. diff --git a/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc index 3a35c60694..6d4f2065de 100644 --- a/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc +++ b/.cursor/rules/product-component-requirements-mdc/product-list-component.mdc @@ -116,4 +116,4 @@ const requirement = { ## 🔍 Analysis Rule -Analyze node_modules/@salesforce/retail-react-app/app/pages/product-list/ to understand how product listing pages are implemented. Study the search functionality, filtering, pagination, and grid/list display patterns. +Analyze "node_modules/@salesforce/*retail-react-app*/app/pages/*product*/" to understand how product listing pages are implemented. Study the search functionality, filtering, pagination, and grid/list display patterns. diff --git a/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc b/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc index c66cbf6e62..19649b1ad1 100644 --- a/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc +++ b/.cursor/rules/product-component-requirements-mdc/product-tile-component.mdc @@ -105,5 +105,5 @@ const requirement = { ## 🔍 Analysis Rule -Analyze node_modules/@salesforce/retail-react-app/app/components/product-tile/ to understand how product tiles are implemented. Study the image handling, pricing display, variant selection, and interaction patterns. +Analyze "node_modules/@salesforce/*retail-react-app*/app/pages/*product*/" to understand how product tiles are implemented. Study the image handling, pricing display, variant selection, and interaction patterns. From d3124b7c680299e9db01aefc045179b1b8e198fe Mon Sep 17 00:00:00 2001 From: Larnelle Ankunda Date: Wed, 2 Jul 2025 12:24:32 -0700 Subject: [PATCH 022/626] updated utility tests --- .../src/utils/opentelemetry.test.js | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js index c6a2e848d9..adc573de08 100644 --- a/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js +++ b/packages/pwa-kit-react-sdk/src/utils/opentelemetry.test.js @@ -197,6 +197,29 @@ describe('OpenTelemetry Utilities', () => { expect(result).toBe(mockSpan) }) + test('should handle performance mark with non-string detail', () => { + const result = opentelemetryUtils.createChildSpan('perf-span', { + performance_mark: 'test-mark', + performance_detail: {key: 'value'}, + other: 'value' + }) + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'perf-span', + { + attributes: { + 'service.name': 'pwa-kit-react-sdk', + 'performance.mark': 'test-mark', + 'performance.type': 'start', + 'performance.detail': '{"key":"value"}', + other: 'value' + } + }, + 'test-context' + ) + expect(result).toBe(mockSpan) + }) + test('should handle errors gracefully', () => { mockTracer.startSpan.mockImplementation(() => { throw new Error('Child span creation failed') @@ -214,6 +237,45 @@ describe('OpenTelemetry Utilities', () => { } }) }) + + test('should return parent span when duplicate performance mark is detected', () => { + // Mock a parent span with matching performance_mark + const parentSpanWithMark = { + ...mockSpan, + attributes: { + performance_mark: 'duplicate-span' + } + } + mockTrace.getSpan.mockReturnValue(parentSpanWithMark) + + const result = opentelemetryUtils.createChildSpan('duplicate-span', { + performance_mark: 'duplicate-span' + }) + + expect(result).toBe(parentSpanWithMark) + expect(mockTracer.startSpan).not.toHaveBeenCalled() + }) + + test('should create span without parent span context', () => { + // Mock no parent span + mockTrace.getSpan.mockReturnValue(null) + + const result = opentelemetryUtils.createChildSpan('child-span', { + test: 'value' + }) + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'child-span', + { + attributes: { + 'service.name': 'pwa-kit-react-sdk', + test: 'value' + } + }, + undefined + ) + expect(result).toBe(mockSpan) + }) }) describe('endSpan', () => { @@ -392,6 +454,29 @@ describe('OpenTelemetry Utilities', () => { ) }) + test('should handle performance mark with non-string detail in metric', () => { + opentelemetryUtils.logPerformanceMetric('test-metric', 150, { + performance_mark: 'test-mark', + performance_detail: {key: 'value'}, + other: 'value' + }) + + expect(mockTracer.startSpan).toHaveBeenCalledWith( + 'test-metric', + { + attributes: { + 'service.name': 'pwa-kit-react-sdk', + 'metric.duration': 150, + 'performance.mark': 'test-mark', + 'performance.type': 'end', + 'performance.detail': '{"key":"value"}', + other: 'value' + } + }, + 'test-context' + ) + }) + test('should warn when no parent span is found', () => { mockTrace.getSpan.mockReturnValue(null) From 7b349b6df5a93e212ed74354c4d12f2b725313d2 Mon Sep 17 00:00:00 2001 From: Jang ho Jung Date: Wed, 2 Jul 2025 15:55:47 -0700 Subject: [PATCH 023/626] Uncomment global.performance --- packages/pwa-kit-react-sdk/setup-jest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pwa-kit-react-sdk/setup-jest.js b/packages/pwa-kit-react-sdk/setup-jest.js index 98410a8d3d..2d94d3e2b1 100644 --- a/packages/pwa-kit-react-sdk/setup-jest.js +++ b/packages/pwa-kit-react-sdk/setup-jest.js @@ -40,4 +40,4 @@ 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 +global.performance = performance From 370178a0eed3870fc1604cf073e5a3ad430b0f2e Mon Sep 17 00:00:00 2001 From: snilakandan Date: Thu, 3 Jul 2025 15:39:21 -0500 Subject: [PATCH 024/626] Cursor Rules for generating components --- .../component-generation-requirements.mdc | 249 ++++++++++++++++++ .../README.md | 27 -- .../product-detail-component.mdc | 97 ------- .../product-list-component.mdc | 119 --------- .../product-tile-component.mdc | 109 -------- .../rules/product-component-requirements.mdc | 34 --- 6 files changed, 249 insertions(+), 386 deletions(-) create mode 100644 .cursor/rules/component-generation-requirements.mdc delete mode 100644 .cursor/rules/product-component-requirements-mdc/README.md delete mode 100644 .cursor/rules/product-component-requirements-mdc/product-detail-component.mdc delete mode 100644 .cursor/rules/product-component-requirements-mdc/product-list-component.mdc delete mode 100644 .cursor/rules/product-component-requirements-mdc/product-tile-component.mdc delete mode 100644 .cursor/rules/product-component-requirements.mdc diff --git a/.cursor/rules/component-generation-requirements.mdc b/.cursor/rules/component-generation-requirements.mdc new file mode 100644 index 0000000000..d858489bfc --- /dev/null +++ b/.cursor/rules/component-generation-requirements.mdc @@ -0,0 +1,249 @@ +--- +description: USE when generating components +globs: "**/components/**" +alwaysApply: false +--- +# Create and Generate React Components in PWA Kit + +## Overview +- Agent must follow the following steps to handle user request to create a new component: + - Agent must ask user to provide the component name, purpose, and desired functionality. + - Agent should determine the appropriate component type (presentational, container, form, etc.). + - Collected component information must be displayed and asked for confirmation. + +## Instructions to Create a New Component + +You need to create a new React component following established patterns and best practices. + +### 1. Create the Component File +First, create a new file for your component. A good practice is to place it in the `app/components` directory. +For example, to create a new "User Profile" component, you would create the file `app/components/user-profile/index.jsx`: + +```jsx +import React from 'react' +import {Box, Avatar, Text} from '@salesforce/retail-react-app/app/components/shared/ui' +import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer' + +const UserProfile = ({userId}) => { + const {data: customer, loading, error} = useCurrentCustomer() + + if (loading) return Loading... + if (error) return Error loading user + if (!customer) return User not found + + return ( + + + + {customer.firstName} {customer.lastName} + + + {customer.email} + + + ) +} + +export default UserProfile +``` + +### 2. Component Structure Guidelines +- **Always use functional components** with React hooks +- **Include proper error handling** for data fetching +- **Use semantic HTML elements** and proper accessibility +- **Follow naming conventions**: PascalCase for components, camelCase for props +- **Include TypeScript interfaces** when using TypeScript + +### 3. File Organization +``` +app/components/ +├── user-profile/ +│ ├── index.jsx // Main component +│ ├── user-profile.test.jsx // Tests +│ └── user-profile.stories.jsx // Storybook +``` + +## Overriding an Existing Component + +Override components from the base template by creating files with the same name and path in your project's `overrides` directory. + +### 1. Identify the Component to Override +The component in the base template is located at `app/components/component-name/index.jsx`. + +### 2. Create the Overriding File +To override this component, create a new file with the exact same path inside your `overrides` directory: `overrides/app/components/component-name/index.jsx`. + +### 3. Customize the Component +Now you can add your custom code to this new file. You can either copy the original content and modify it, extend the original component or create a new component from scratch. + +Here's an example of how you might add a custom feature to extend an existing component: + +```jsx +// overrides/app/components/product-card/index.jsx +import React from 'react' +import {Box, Badge} from '@salesforce/retail-react-app/app/components/shared/ui' +import {default as BaseProductCard} from '@salesforce/retail-react-app/app/components/product-card' + +const CustomProductCard = (props) => { + return ( + + + New! + + + + ) +} + +// You must export the component as default +export default CustomProductCard +``` + +In this example, we're importing the original `ProductCard` component and wrapping it with our own custom component to add a "New!" badge. + +## Component Best Practices + +### Component Structure +- **Single Responsibility**: One main purpose per component +- **Functional Components**: Always use hooks-based approach +- **Error Handling**: Include loading and error states +- **Accessibility**: Use semantic HTML and ARIA labels +- **Type Safety**: Include TypeScript interfaces when possible + +### Naming Conventions +- **Components**: PascalCase (e.g., `UserProfile`, `ProductCard`) +- **Files**: kebab-case for directories (e.g., `user-profile/`) +- **Props**: camelCase (e.g., `userId`, `productName`) +- **Hooks**: camelCase starting with 'use' (e.g., `useUser`, `useProduct`) + +### File Organization +- Place components in `app/components/` directory +- Use index.jsx for main component file +- Include test files with `.test.jsx` suffix +- Include Storybook files with `.stories.jsx` suffix + +## Common Patterns to Avoid + +### Component Anti-patterns +- **Class Components**: Use functional components with hooks +- **Large Components**: Break down components over 200 lines +- **Mixed Concerns**: Don't mix UI logic with business logic +- **Poor Error Handling**: Always handle loading and error states +- **Missing Accessibility**: Include proper ARIA labels and semantic HTML + +## Special Component Types + +The PWA Kit also has several "special" component types that you can create for different purposes: + +### Presentational Components +Components that focus on displaying data without business logic: +```jsx +// app/components/product-image/index.jsx +import React from 'react' +import {Image} from '@salesforce/retail-react-app/app/components/shared/ui' + +const ProductImage = ({src, alt, ...props}) => { + return {alt} +} + +export default ProductImage +``` + +### Container Components +Components that handle data fetching and state management: +```jsx +// app/components/product-details-container/index.jsx +import React from 'react' +import {useProduct} from '@salesforce/retail-react-app/app/hooks/use-product' +import ProductDetails from './product-details' + +const ProductDetailsContainer = ({productId}) => { + const {data: product, loading, error} = useProduct(productId) + + if (loading) return
Loading...
+ if (error) return
Error loading product
+ + return +} + +export default ProductDetailsContainer +``` + +### Form Components +Components that handle user input and form submission: +```jsx +// app/components/contact-form/index.jsx +import React from 'react' +import {Box, Button, Input, Textarea} from '@salesforce/retail-react-app/app/components/shared/ui' + +const ContactForm = ({onSubmit}) => { + const handleSubmit = (e) => { + e.preventDefault() + // Handle form submission + onSubmit(formData) + } + + return ( + + + +