@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 +14 -0
- package/README.md +51 -0
- package/package.json +1 -1
- package/src/aggregation/aggregation-strategies.js +248 -0
- package/src/index.d.ts +30 -0
- package/src/index.js +13 -1
- package/src/log-wrapper.js +30 -14
- package/src/sqs.js +34 -3
- package/src/xray.js +45 -0
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
|
@@ -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';
|
package/src/log-wrapper.js
CHANGED
|
@@ -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
|
|
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`.
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
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.
|
|
29
|
-
*
|
|
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
|
-
|
|
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
|
|
44
|
-
context.
|
|
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](
|
|
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:
|
|
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
|
+
}
|