@adobe/spacecat-shared-utils 1.74.0 → 1.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [@adobe/spacecat-shared-utils-v1.76.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.75.0...@adobe/spacecat-shared-utils-v1.76.0) (2025-11-20)
2
+
3
+
4
+ ### Features
5
+
6
+ * **utils,http-utils:** trigger release for trace ID propagation ([#1154](https://github.com/adobe/spacecat-shared/issues/1154)) ([fb48149](https://github.com/adobe/spacecat-shared/commit/fb481497725e49dab18812b4ba1ba7186e35a8f9)), closes [#1152](https://github.com/adobe/spacecat-shared/issues/1152) [#1152](https://github.com/adobe/spacecat-shared/issues/1152) [#1097](https://github.com/adobe/spacecat-shared/issues/1097) [#1097](https://github.com/adobe/spacecat-shared/issues/1097) [#1097](https://github.com/adobe/spacecat-shared/issues/1097) [#1152](https://github.com/adobe/spacecat-shared/issues/1152) [adobe/spacecat-audit-worker#1520](https://github.com/adobe/spacecat-audit-worker/issues/1520)
7
+
8
+ # [@adobe/spacecat-shared-utils-v1.75.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.74.0...@adobe/spacecat-shared-utils-v1.75.0) (2025-11-19)
9
+
10
+
11
+ ### Features
12
+
13
+ * add aggregation key to get Oppty SC API ([#1148](https://github.com/adobe/spacecat-shared/issues/1148)) ([07bf485](https://github.com/adobe/spacecat-shared/commit/07bf485a5534a066899abf9488ef71301d8cb3e1))
14
+
1
15
  # [@adobe/spacecat-shared-utils-v1.74.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.73.1...@adobe/spacecat-shared-utils-v1.74.0) (2025-11-17)
2
16
 
3
17
 
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  This repository contains a collection of shared utility functions used across various SpaceCat projects. These utilities provide a range of checks and validations, from basic data type validation to more complex checks like ISO date strings and URL validation.
4
4
 
5
+ > **v1.76.0**: Added trace ID propagation support for distributed tracing across SpaceCat services.
6
+
5
7
  ## Installation
6
8
 
7
9
  To install the SpaceCat Shared Utilities, you can use npm:
@@ -45,6 +47,40 @@ The library includes the following utility functions:
45
47
  - `hasText(str)`: Checks if the given string is not empty.
46
48
  - `dateAfterDays(number)`: Calculates the date after a specified number of days from the current date.
47
49
 
50
+ ## Log Wrapper
51
+
52
+ The `logWrapper` enhances your Lambda function logs by automatically prepending `jobId` (from message) and `traceId` (from AWS X-Ray) to all log statements. This improves log traceability across distributed services.
53
+
54
+ ### Features
55
+ - Automatically extracts AWS X-Ray trace ID
56
+ - Includes jobId from message when available
57
+ - Enhances `context.log` directly - **no code changes needed**
58
+ - Works seamlessly with existing log levels (info, error, debug, warn, trace, etc.)
59
+
60
+ ### Usage
61
+
62
+ ```javascript
63
+ import { logWrapper, sqsEventAdapter } from '@adobe/spacecat-shared-utils';
64
+
65
+ async function run(message, context) {
66
+ const { log } = context;
67
+
68
+ // Use context.log as usual - trace IDs are added automatically
69
+ log.info('Processing started');
70
+ // Output: [jobId=xxx] [traceId=1-xxx-xxx] Processing started
71
+ }
72
+
73
+ export const main = wrap(run)
74
+ .with(sqsEventAdapter)
75
+ .with(logWrapper) // Add this line early in the wrapper chain
76
+ .with(dataAccess)
77
+ .with(sqs)
78
+ .with(secrets)
79
+ .with(helixStatus);
80
+ ```
81
+
82
+ **Note:** The `logWrapper` enhances `context.log` directly. All existing code using `context.log` will automatically include trace IDs and job IDs in logs without any code changes.
83
+
48
84
  ## SQS Event Adapter
49
85
 
50
86
  The library also includes an SQS event adapter to convert an SQS record into a function parameter. This is useful when working with AWS Lambda functions that are triggered by an SQS event. Usage:
@@ -62,6 +98,21 @@ export const main = wrap(run)
62
98
  .with(helixStatus);
63
99
  ````
64
100
 
101
+ ## AWS X-Ray Integration
102
+
103
+ ### getTraceId()
104
+
105
+ Extracts the current AWS X-Ray trace ID from the segment. Returns `null` if not in AWS Lambda or no segment is available.
106
+
107
+ ```javascript
108
+ import { getTraceId } from '@adobe/spacecat-shared-utils';
109
+
110
+ const traceId = getTraceId();
111
+ // Returns: '1-5e8e8e8e-5e8e8e8e5e8e8e8e5e8e8e8e' or null
112
+ ```
113
+
114
+ This function is automatically used by `logWrapper` to include trace IDs in logs.
115
+
65
116
  ## Testing
66
117
 
67
118
  This library includes a comprehensive test suite to ensure the reliability of the utility functions. To run the tests, use the following command:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-utils",
3
- "version": "1.74.0",
3
+ "version": "1.76.0",
4
4
  "description": "Shared modules of the Spacecat Services - utils",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,248 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ /**
14
+ * Accessibility suggestion aggregation strategies
15
+ *
16
+ * Defines how HTML elements with accessibility issues are grouped into database suggestions.
17
+ * Each granularity level has a function that builds an aggregation key from suggestion data.
18
+ */
19
+
20
+ /**
21
+ * Granularity levels for suggestion aggregation
22
+ * @enum {string}
23
+ */
24
+ export const Granularity = {
25
+ /** One suggestion per HTML element - url|type|selector (e.g., page1|color-contrast|div.header) */
26
+ INDIVIDUAL: 'INDIVIDUAL',
27
+
28
+ /**
29
+ * One suggestion per issue type per page - url|type (e.g., page1|color-contrast)
30
+ */
31
+ PER_PAGE_PER_COMPONENT: 'PER_PAGE_PER_COMPONENT',
32
+
33
+ /** One suggestion per page - url (e.g., page1) */
34
+ PER_PAGE: 'PER_PAGE',
35
+
36
+ /**
37
+ * One suggestion per component type across all pages - type|selector
38
+ */
39
+ PER_COMPONENT: 'PER_COMPONENT',
40
+
41
+ /** One suggestion per issue type globally - type (e.g., color-contrast) */
42
+ PER_TYPE: 'PER_TYPE',
43
+ };
44
+
45
+ /**
46
+ * Generic key builder that concatenates non-empty values with pipe separator
47
+ * @param {...string} parts - Variable number of key parts to concatenate
48
+ * @returns {string} Concatenated key
49
+ * @private - exported for testing purposes
50
+ */
51
+ export function buildKey(...parts) {
52
+ return parts.filter((part) => part != null && part !== '').join('|');
53
+ }
54
+
55
+ /**
56
+ * Builds aggregation key for INDIVIDUAL granularity
57
+ * Key format: url|type|selector|source
58
+ * @private - exported for testing purposes
59
+ */
60
+ export function buildIndividualKey({
61
+ url, issueType, targetSelector, source,
62
+ }) {
63
+ return buildKey(url, issueType, targetSelector, source);
64
+ }
65
+
66
+ /**
67
+ * Builds aggregation key for PER_PAGE_PER_COMPONENT granularity
68
+ * Key format: url|type|source
69
+ */
70
+ function buildPerPagePerComponentKey({ url, issueType, source }) {
71
+ return buildKey(url, issueType, source);
72
+ }
73
+
74
+ /**
75
+ * Builds aggregation key for PER_PAGE granularity
76
+ * Key format: url|source
77
+ */
78
+ function buildPerPageKey({ url, source }) {
79
+ return buildKey(url, source);
80
+ }
81
+
82
+ /**
83
+ * Builds aggregation key for COMPONENT granularity
84
+ * Key format: type|selector
85
+ */
86
+ function buildComponentKey({ issueType, targetSelector }) {
87
+ return buildKey(issueType, targetSelector);
88
+ }
89
+
90
+ /**
91
+ * Builds aggregation key for GLOBAL granularity
92
+ * Key format: type
93
+ */
94
+ function buildGlobalKey({ issueType }) {
95
+ return buildKey(issueType);
96
+ }
97
+
98
+ /**
99
+ * Registry of key-building functions by granularity level
100
+ */
101
+ export const GRANULARITY_KEY_BUILDERS = {
102
+ [Granularity.INDIVIDUAL]: buildIndividualKey,
103
+ [Granularity.PER_PAGE_PER_COMPONENT]: buildPerPagePerComponentKey,
104
+ [Granularity.PER_PAGE]: buildPerPageKey,
105
+ [Granularity.PER_COMPONENT]: buildComponentKey,
106
+ [Granularity.PER_TYPE]: buildGlobalKey,
107
+ };
108
+
109
+ /**
110
+ * Maps issue types to their aggregation granularity
111
+ * Based on the nature of each issue and how they should be grouped
112
+ */
113
+ export const ISSUE_GRANULARITY_MAP = {
114
+ 'color-contrast': Granularity.INDIVIDUAL,
115
+ list: Granularity.PER_COMPONENT,
116
+ 'aria-roles': Granularity.PER_PAGE_PER_COMPONENT,
117
+ 'image-alt': Granularity.PER_PAGE_PER_COMPONENT,
118
+ 'link-in-text-block': Granularity.PER_PAGE_PER_COMPONENT,
119
+ 'link-name': Granularity.PER_PAGE_PER_COMPONENT,
120
+ 'target-size': Granularity.PER_PAGE_PER_COMPONENT,
121
+ listitem: Granularity.PER_COMPONENT,
122
+ label: Granularity.PER_PAGE_PER_COMPONENT,
123
+ 'aria-prohibited-attr': Granularity.PER_TYPE,
124
+ 'button-name': Granularity.PER_PAGE_PER_COMPONENT,
125
+ 'frame-title': Granularity.PER_PAGE_PER_COMPONENT,
126
+ 'aria-valid-attr-value': Granularity.PER_PAGE_PER_COMPONENT,
127
+ 'aria-allowed-attr': Granularity.PER_TYPE,
128
+ 'aria-hidden-focus': Granularity.PER_PAGE_PER_COMPONENT,
129
+ 'nested-interactive': Granularity.PER_PAGE_PER_COMPONENT,
130
+ 'html-has-lang': Granularity.PER_PAGE,
131
+ 'meta-viewport': Granularity.PER_PAGE,
132
+ 'aria-required-children': Granularity.PER_PAGE_PER_COMPONENT,
133
+ 'aria-required-parent': Granularity.PER_PAGE_PER_COMPONENT,
134
+ 'meta-refresh': Granularity.PER_PAGE,
135
+ 'role-img-alt': Granularity.PER_PAGE_PER_COMPONENT,
136
+ 'aria-input-field-name': Granularity.PER_PAGE_PER_COMPONENT,
137
+ 'scrollable-region-focusable': Granularity.PER_PAGE_PER_COMPONENT,
138
+ 'select-name': Granularity.PER_PAGE_PER_COMPONENT,
139
+ };
140
+
141
+ /**
142
+ * Gets the granularity level for a specific issue type
143
+ *
144
+ * @param {string} issueType - The issue type (e.g., "color-contrast")
145
+ * @returns {string} The granularity level (defaults to PER_PAGE_PER_COMPONENT)
146
+ */
147
+ export function getGranularityForIssueType(issueType) {
148
+ return ISSUE_GRANULARITY_MAP[issueType] || Granularity.PER_PAGE_PER_COMPONENT;
149
+ }
150
+
151
+ /**
152
+ * Builds an aggregation key for grouping HTML elements during processing
153
+ *
154
+ * @param {string} issueType - The issue type
155
+ * @param {string} url - Page URL
156
+ * @param {string} targetSelector - CSS selector for the element
157
+ * @param {string} source - Optional source identifier
158
+ * @returns {string} The aggregation key based on the issue type's granularity
159
+ */
160
+ export function buildAggregationKey(issueType, url, targetSelector, source) {
161
+ const granularity = getGranularityForIssueType(issueType);
162
+ const keyBuilder = GRANULARITY_KEY_BUILDERS[granularity];
163
+
164
+ /* c8 ignore start - defensive code */
165
+ if (!keyBuilder) {
166
+ return buildIndividualKey({
167
+ url, issueType, targetSelector, source,
168
+ });
169
+ }
170
+ /* c8 ignore stop */
171
+
172
+ return keyBuilder({
173
+ url, issueType, targetSelector, source,
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Builds an aggregation key from suggestion data.
179
+ * Extracts the necessary fields from a suggestion object and calls buildAggregationKey.
180
+ *
181
+ * @param {Object} suggestionData - The suggestion data object
182
+ * @param {string} suggestionData.url - Page URL
183
+ * @param {Array} suggestionData.issues - Array of issues
184
+ * @param {string} suggestionData.source - Optional source
185
+ * @returns {string|null} The aggregation key based on the issue type's granularity,
186
+ * or null if no issues
187
+ */
188
+ export function buildAggregationKeyFromSuggestion(suggestionData) {
189
+ // Handle null, undefined, or non-object inputs
190
+ if (!suggestionData || typeof suggestionData !== 'object') {
191
+ return null;
192
+ }
193
+
194
+ const { url, issues, source } = suggestionData;
195
+
196
+ if (!issues || issues.length === 0) {
197
+ return null;
198
+ }
199
+
200
+ const firstIssue = issues[0];
201
+ if (!firstIssue || !firstIssue.type) {
202
+ return null;
203
+ }
204
+
205
+ const issueType = firstIssue.type;
206
+ const htmlWithIssue = firstIssue.htmlWithIssues?.[0];
207
+ // Support both snake_case and camelCase for backwards compatibility
208
+ const targetSelector = htmlWithIssue?.target_selector || htmlWithIssue?.targetSelector || '';
209
+
210
+ return buildAggregationKey(issueType, url, targetSelector, source);
211
+ }
212
+
213
+ /**
214
+ * Builds a database-level key for matching suggestions across audit runs.
215
+ * Used by syncSuggestions to identify existing suggestions.
216
+ *
217
+ * This ALWAYS uses INDIVIDUAL granularity (url|type|selector|source) to ensure
218
+ * each HTML element gets its own suggestion in the database. This prevents
219
+ * incorrect merging of different HTML elements.
220
+ *
221
+ * IMPORTANT: This maintains backwards compatibility with the original buildKey logic
222
+ * by including a trailing pipe when selector is empty (url|type|).
223
+ *
224
+ * @param {Object} suggestionData - The suggestion data object
225
+ * @param {string} suggestionData.url - Page URL
226
+ * @param {Array} suggestionData.issues - Array of issues
227
+ * @param {string} suggestionData.source - Optional source
228
+ * @returns {string} The key for suggestion matching
229
+ */
230
+ export function buildSuggestionKey(suggestionData) {
231
+ const { url, issues, source } = suggestionData;
232
+
233
+ if (!issues || issues.length === 0) {
234
+ return url;
235
+ }
236
+
237
+ const firstIssue = issues[0];
238
+ const issueType = firstIssue.type;
239
+ const targetSelector = firstIssue.htmlWithIssues?.[0]?.target_selector || '';
240
+
241
+ // Always build INDIVIDUAL-level key for database uniqueness
242
+ // Backwards compatible: url|type|selector|source or url|type| when selector is empty
243
+ let key = `${url}|${issueType}|${targetSelector}`;
244
+ if (source) {
245
+ key += `|${source}`;
246
+ }
247
+ return key;
248
+ }
package/src/index.d.ts CHANGED
@@ -66,6 +66,36 @@ export function sqsWrapper(fn: (message: object, context: object) => Promise<Res
66
66
  export function sqsEventAdapter(fn: (message: object, context: object) => Promise<Response>):
67
67
  (request: object, context: object) => Promise<Response>;
68
68
 
69
+ /**
70
+ * A higher-order function that wraps a given function and enhances logging by appending
71
+ * a `jobId` and `traceId` to log messages when available.
72
+ * @param fn - The original function to be wrapped
73
+ * @returns A wrapped function that enhances logging
74
+ */
75
+ export function logWrapper(fn: (message: object, context: object) => Promise<Response>):
76
+ (message: object, context: object) => Promise<Response>;
77
+
78
+ /**
79
+ * Instruments an AWS SDK v3 client with X-Ray tracing when running in AWS Lambda.
80
+ * @param client - The AWS SDK v3 client to instrument
81
+ * @returns The instrumented client (or original client if not in Lambda)
82
+ */
83
+ export function instrumentAWSClient<T>(client: T): T;
84
+
85
+ /**
86
+ * Extracts the trace ID from the current AWS X-Ray segment.
87
+ * @returns The trace ID if available, or null if not in AWS Lambda or no segment found
88
+ */
89
+ export function getTraceId(): string | null;
90
+
91
+ /**
92
+ * Adds the x-trace-id header to a headers object if a trace ID is available.
93
+ * @param headers - The headers object to augment
94
+ * @param context - The context object that may contain traceId
95
+ * @returns The headers object with x-trace-id added if available
96
+ */
97
+ export function addTraceIdHeader(headers?: Record<string, string>, context?: object): Record<string, string>;
98
+
69
99
  /**
70
100
  * Prepends 'https://' schema to the URL if it's not already present.
71
101
  * @param url - The URL to modify.
package/src/index.js CHANGED
@@ -52,7 +52,7 @@ export { sqsWrapper } from './sqs.js';
52
52
  export { sqsEventAdapter } from './sqs.js';
53
53
 
54
54
  export { logWrapper } from './log-wrapper.js';
55
- export { instrumentAWSClient } from './xray.js';
55
+ export { instrumentAWSClient, getTraceId, addTraceIdHeader } from './xray.js';
56
56
 
57
57
  export {
58
58
  composeBaseURL,
@@ -108,3 +108,15 @@ export * as schemas from './schemas.js';
108
108
 
109
109
  export { detectLocale } from './locale-detect/locale-detect.js';
110
110
  export { prettifyLogForwardingConfig } from './cdn-helpers.js';
111
+
112
+ export {
113
+ buildAggregationKey,
114
+ buildAggregationKeyFromSuggestion,
115
+ buildSuggestionKey,
116
+ buildIndividualKey,
117
+ buildKey,
118
+ getGranularityForIssueType,
119
+ Granularity,
120
+ GRANULARITY_KEY_BUILDERS,
121
+ ISSUE_GRANULARITY_MAP,
122
+ } from './aggregation/aggregation-strategies.js';
@@ -10,47 +10,63 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import { getTraceId } from './xray.js';
14
+
13
15
  /**
14
16
  * A higher-order function that wraps a given function and enhances logging by appending
15
- * a `jobId` to log messages when available. This improves traceability of logs associated
16
- * with specific jobs or processes.
17
+ * a `jobId` and `traceId` to log messages when available. This improves traceability of logs
18
+ * associated with specific jobs or processes.
17
19
  *
18
20
  * The wrapper checks if a `log` object exists in the `context` and whether the `message`
19
- * contains a `jobId`. If found, log methods (e.g., `info`, `error`, etc.) will prepend the
20
- * `jobId` to all log statements where `context.contextualLog` is used. If no `jobId` is found,
21
- * logging will remain unchanged.
21
+ * contains a `jobId`. It also extracts the AWS X-Ray trace ID if available. If found, log
22
+ * methods (e.g., `info`, `error`, etc.) will prepend the `jobId` and/or `traceId` to all log
23
+ * statements. All existing code using `context.log` will automatically include these markers.
22
24
  *
23
25
  * @param {function} fn - The original function to be wrapped, called with the provided
24
26
  * message and context after logging enhancement.
25
27
  * @returns {function(object, object): Promise<Response>} - A wrapped function that enhances
26
28
  * logging and returns the result of the original function.
27
29
  *
28
- * `context.contextualLog` will include logging methods with `jobId` prefixed, or fall back
29
- * to the existing `log` object if no `jobId` is provided.
30
+ * `context.log` will be enhanced in place to include `jobId` and/or `traceId` prefixed to all
31
+ * log messages. No code changes needed - existing `context.log` calls work automatically.
30
32
  */
31
33
  export function logWrapper(fn) {
32
34
  return async (message, context) => {
33
35
  const { log } = context;
34
36
 
35
37
  if (log && !context.contextualLog) {
38
+ const markers = [];
39
+
40
+ // Extract jobId from message if available
36
41
  if (typeof message === 'object' && message !== null && 'jobId' in message) {
37
42
  const { jobId } = message;
38
- const jobIdMarker = `[jobId=${jobId}]`;
43
+ markers.push(`[jobId=${jobId}]`);
44
+ }
45
+
46
+ // Extract traceId from AWS X-Ray
47
+ const traceId = getTraceId();
48
+ if (traceId) {
49
+ markers.push(`[traceId=${traceId}]`);
50
+ }
51
+
52
+ // If we have markers, enhance the log object directly
53
+ if (markers.length > 0) {
54
+ const markerString = markers.join(' ');
39
55
 
40
56
  // Define log levels
41
57
  const logLevels = ['info', 'error', 'debug', 'warn', 'trace', 'verbose', 'silly', 'fatal'];
42
58
 
43
- // Enhance the log object to include jobId in all log statements
44
- context.contextualLog = logLevels.reduce((accumulator, level) => {
59
+ // Enhance context.log directly to include markers in all log statements
60
+ context.log = logLevels.reduce((accumulator, level) => {
45
61
  if (typeof log[level] === 'function') {
46
- accumulator[level] = (...args) => log[level](jobIdMarker, ...args);
62
+ accumulator[level] = (...args) => log[level](markerString, ...args);
47
63
  }
48
64
  return accumulator;
49
65
  }, {});
50
- } else {
51
- log.debug('No jobId found in the provided message. Log entries will be recorded without a jobId.');
52
- context.contextualLog = log;
53
66
  }
67
+
68
+ // Mark that we've processed this context
69
+ context.contextualLog = context.log;
54
70
  }
55
71
 
56
72
  return fn(message, context);
package/src/sqs.js CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { Response } from '@adobe/fetch';
14
14
  import { SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs';
15
- import { instrumentAWSClient } from './xray.js';
15
+ import { instrumentAWSClient, getTraceId } from './xray.js';
16
16
 
17
17
  import { hasText, isNonEmptyArray } from './functions.js';
18
18
  import { isAWSLambda } from './runtimes.js';
@@ -46,8 +46,16 @@ class SQS {
46
46
 
47
47
  /**
48
48
  * Send a message to an SQS queue. For FIFO queues, messageGroupId is required.
49
+ * Automatically includes traceId in the message payload if available from:
50
+ * 1. The message itself (if explicitly set by caller, e.g. from context.traceId)
51
+ * 2. AWS X-Ray segment (current Lambda execution trace)
52
+ *
53
+ * Special handling for Jobs Dispatcher and similar scenarios:
54
+ * - Set traceId to null to opt-out of trace propagation (each worker gets its own trace)
55
+ *
49
56
  * @param {string} queueUrl - The URL of the SQS queue.
50
57
  * @param {object} message - The message body to send.
58
+ * Can include traceId for propagation or set to null to opt-out.
51
59
  * @param {string} messageGroupId - (Optional) The message group ID for FIFO queues.
52
60
  * @return {Promise<void>}
53
61
  */
@@ -57,6 +65,23 @@ class SQS {
57
65
  timestamp: new Date().toISOString(),
58
66
  };
59
67
 
68
+ // Handle traceId based on explicit setting or auto-generation
69
+ // Three cases:
70
+ // 1. Property not in message → auto-add X-Ray traceId
71
+ // 2. Property set to null → explicit opt-out (e.g., Jobs Dispatcher)
72
+ // 3. Property has a value → use that value
73
+ if (!('traceId' in message)) {
74
+ // Case 1: No traceId property - auto-add X-Ray trace
75
+ const traceId = getTraceId();
76
+ if (traceId) {
77
+ body.traceId = traceId;
78
+ }
79
+ } else if (message.traceId === null) {
80
+ // Case 2: Explicitly null - opt-out of trace propagation
81
+ delete body.traceId;
82
+ }
83
+ // Case 3: Has a value - already in body from spread, keep it
84
+
60
85
  const params = {
61
86
  MessageBody: JSON.stringify(body),
62
87
  QueueUrl: queueUrl,
@@ -71,7 +96,7 @@ class SQS {
71
96
 
72
97
  try {
73
98
  const data = await this.sqsClient.send(msgCommand);
74
- this.log.debug(`Success, message sent. MessageID: ${data.MessageId}`);
99
+ this.log.debug(`Success, message sent. MessageID: ${data.MessageId}${body.traceId ? `, TraceID: ${body.traceId}` : ''}`);
75
100
  } catch (e) {
76
101
  const { type, code, message: msg } = e;
77
102
  this.log.error(`Message sent failed. Type: ${type}, Code: ${code}, Message: ${msg}`);
@@ -95,6 +120,7 @@ export function sqsWrapper(fn) {
95
120
  /**
96
121
  * Wrapper to turn an SQS record into a function param
97
122
  * Inspired by https://github.com/adobe/helix-admin/blob/main/src/index.js#L108-L133
123
+ * Extracts traceId from the message payload if present and stores it in context for propagation.
98
124
  *
99
125
  * @param {UniversalAction} fn
100
126
  * @returns {function(object, UniversalContext): Promise<Response>}
@@ -124,7 +150,12 @@ export function sqsEventAdapter(fn) {
124
150
 
125
151
  try {
126
152
  message = JSON.parse(record.body);
127
- log.debug(`Received message with id: ${record.messageId}`);
153
+ log.debug(`Received message with id: ${record.messageId}${message.traceId ? `, traceId: ${message.traceId}` : ''}`);
154
+
155
+ // Store traceId in context if present in the message for downstream propagation
156
+ if (message.traceId) {
157
+ context.traceId = message.traceId;
158
+ }
128
159
  } catch (e) {
129
160
  log.warn('Function was not invoked properly, message body is not a valid JSON', e);
130
161
  return badRequest('Event does not contain a valid message body');
package/src/xray.js CHANGED
@@ -16,3 +16,48 @@ import { isAWSLambda } from './runtimes.js';
16
16
  export function instrumentAWSClient(client) {
17
17
  return isAWSLambda() ? AWSXray.captureAWSv3Client(client) : client;
18
18
  }
19
+
20
+ /**
21
+ * Extracts the trace ID from the current AWS X-Ray segment.
22
+ * This function is designed to work in AWS Lambda environments where X-Ray tracing is enabled.
23
+ *
24
+ * @returns {string|null} The trace ID if available, or null if not in Lambda or no segment
25
+ */
26
+ export function getTraceId() {
27
+ if (!isAWSLambda()) {
28
+ return null;
29
+ }
30
+
31
+ const segment = AWSXray.getSegment();
32
+ if (!segment) {
33
+ return null;
34
+ }
35
+
36
+ // Get the root trace ID
37
+ const effectiveSegment = segment.segment || segment;
38
+ return effectiveSegment.trace_id;
39
+ }
40
+
41
+ /**
42
+ * Adds the x-trace-id header to a headers object if a trace ID is available.
43
+ * Checks for traceId from:
44
+ * 1. Explicit context.traceId (from incoming HTTP request or SQS message)
45
+ * 2. AWS X-Ray segment (current Lambda execution)
46
+ *
47
+ * @param {object} headers - The headers object to augment
48
+ * @param {object} context - The context object that may contain traceId
49
+ * @returns {object} The headers object with x-trace-id added if available
50
+ */
51
+ export function addTraceIdHeader(headers = {}, context = {}) {
52
+ // Priority: 1) context.traceId (propagated from incoming request), 2) X-Ray traceId
53
+ const traceId = context.traceId || getTraceId();
54
+
55
+ if (traceId) {
56
+ return {
57
+ ...headers,
58
+ 'x-trace-id': traceId,
59
+ };
60
+ }
61
+
62
+ return headers;
63
+ }