@friggframework/core 2.0.0-next.111 → 2.0.0-next.113

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.
Files changed (74) hide show
  1. package/.eslintrc.json +18 -1
  2. package/CLAUDE.md +101 -13
  3. package/README.md +19 -22
  4. package/core/CLAUDE.md +28 -13
  5. package/core/Worker.js +32 -37
  6. package/core/create-handler.js +97 -197
  7. package/core/index.js +8 -1
  8. package/core/invocation-scope.js +225 -0
  9. package/core/secrets-to-env.js +7 -6
  10. package/database/config.js +0 -7
  11. package/database/documentdb-encryption-service.js +6 -2
  12. package/database/documentdb-utils.js +2 -1
  13. package/database/encryption/README.md +25 -1
  14. package/database/encryption/encryption-schema-registry.js +3 -0
  15. package/database/encryption/integration-mapping-encryption.js +97 -0
  16. package/database/encryption/logger.js +5 -2
  17. package/database/encryption/prisma-encryption-extension.js +20 -9
  18. package/errors/fetch-error.js +48 -65
  19. package/handlers/app-definition-loader.js +38 -2
  20. package/handlers/app-handler-helpers.js +18 -4
  21. package/handlers/backend-utils.js +10 -13
  22. package/handlers/database-migration-handler.js +10 -1
  23. package/handlers/integration-event-dispatcher.js +2 -2
  24. package/handlers/routers/integration-webhook-routers.js +13 -6
  25. package/handlers/routers/websocket.js +27 -7
  26. package/handlers/workers/db-migration.js +12 -1
  27. package/handlers/workers/dlq-processor.js +64 -38
  28. package/index.js +19 -1
  29. package/integrations/integration-base.js +56 -38
  30. package/integrations/integration-router.js +21 -20
  31. package/integrations/repositories/integration-mapping-query-pipeline.js +152 -0
  32. package/integrations/repositories/integration-mapping-query.js +193 -0
  33. package/integrations/repositories/integration-mapping-repository-documentdb.js +53 -0
  34. package/integrations/repositories/integration-mapping-repository-interface.js +48 -0
  35. package/integrations/repositories/integration-mapping-repository-mongo.js +72 -0
  36. package/integrations/repositories/integration-mapping-repository-postgres.js +118 -0
  37. package/integrations/use-cases/delete-integration-for-user.js +24 -13
  38. package/jest.config.js +2 -0
  39. package/logs/.eslintrc.json +26 -1
  40. package/logs/__fixtures__/events.js +82 -0
  41. package/logs/__fixtures__/matchers.js +45 -0
  42. package/logs/__fixtures__/secrets.js +59 -0
  43. package/logs/__fixtures__/vectors.js +329 -0
  44. package/logs/__fixtures__/with-env.js +45 -0
  45. package/logs/context.js +109 -0
  46. package/logs/contract-keys.js +67 -0
  47. package/logs/debug-shims.js +46 -0
  48. package/logs/denied-keys.js +21 -0
  49. package/logs/index.js +14 -3
  50. package/logs/jest-logger-setup.js +25 -0
  51. package/logs/levels.js +71 -0
  52. package/logs/logger-runtime.js +137 -0
  53. package/logs/logger.js +134 -42
  54. package/logs/record.js +276 -0
  55. package/logs/redact.js +328 -0
  56. package/logs/serialize.js +329 -0
  57. package/logs/sinks.js +93 -0
  58. package/logs/summarize-event.js +163 -0
  59. package/modules/module.js +32 -19
  60. package/modules/requester/oauth-2.js +28 -22
  61. package/modules/requester/requester.js +69 -6
  62. package/package.json +5 -6
  63. package/queues/queue-delivery.js +41 -0
  64. package/queues/queuer-util.js +1 -15
  65. package/telemetry/bind-telemetry-context.js +4 -0
  66. package/telemetry/instrument-handler.js +19 -2
  67. package/telemetry/no-op-telemetry.js +4 -0
  68. package/telemetry/otel-telemetry.js +25 -6
  69. package/telemetry/telemetry-context.js +19 -8
  70. package/telemetry/telemetry-runtime.js +17 -0
  71. package/telemetry/telemetry-service.js +1 -0
  72. package/types/errors/index.d.ts +24 -10
  73. package/types/logs/index.d.ts +61 -2
  74. package/user/repositories/user-repository-documentdb.js +10 -42
package/.eslintrc.json CHANGED
@@ -1,3 +1,20 @@
1
1
  {
2
- "extends": "@friggframework/eslint-config"
2
+ "extends": "@friggframework/eslint-config",
3
+ "rules": {
4
+ "no-restricted-syntax": [
5
+ "warn",
6
+ {
7
+ "selector": "CallExpression[callee.object.object.name='process'][callee.object.property.name=/^(stdout|stderr)$/][callee.property.name='write']",
8
+ "message": "Write through the logger; only logs/sinks.js writes to fd 1 (ADR-048 §14)"
9
+ }
10
+ ]
11
+ },
12
+ "overrides": [
13
+ {
14
+ "files": ["*.test.js", "database/utils/prisma-runner.js"],
15
+ "rules": {
16
+ "no-restricted-syntax": "off"
17
+ }
18
+ }
19
+ ]
3
20
  }
package/CLAUDE.md CHANGED
@@ -211,7 +211,56 @@ packages/core/
211
211
  - `integration-repository-factory.js` - Creates database-specific repositories
212
212
  - `integration-repository-mongo.js` - MongoDB implementation
213
213
  - `integration-repository-postgres.js` - PostgreSQL implementation
214
- - `integration-mapping-repository-*.js` - Mapping data persistence
214
+ - `integration-mapping-repository-*.js` - Mapping data persistence.
215
+ `queryMappings(integrationId, { where, orderBy, skip, take, omit })`
216
+ returns `{ mappings, total }`: one filtered, ordered page of an
217
+ integration's mappings, for callers that cannot load every row through
218
+ `findMappingsByIntegration`. `where` is an array of ANDed conditions
219
+ (at most 20); an entry may be `{ anyOf: [...] }`, ORed, one level deep.
220
+ Conditions are `{ path: 'mapping.<segment>...', op: 'exists' | 'notExists' }`
221
+ (JSON null counts as absent), `{ path: 'mapping.…', op: 'in', value: string[] }`
222
+ (1–500 strings) and `{ path: 'sourceId', op: 'notStartsWith', value }`
223
+ (a NULL sourceId matches). `orderBy` is `{ path: 'mapping.…', direction:
224
+ 'asc' | 'desc' }`, nulls last, ties broken by id in the same direction;
225
+ without it rows come in id order. `take` is 1–500. `omit` lists top-level
226
+ mapping keys to leave out of the rows; never write such rows back. Path
227
+ segments must match `^[A-Za-z_][A-Za-z0-9_]*$`. The PostgreSQL, MongoDB and
228
+ DocumentDB adapters give the same pages and totals;
229
+ `integration-mapping-repository-query-parity.test.js` checks that against
230
+ real databases when `QUERY_MAPPINGS_PARITY_MONGO_URL` /
231
+ `QUERY_MAPPINGS_PARITY_POSTGRES_URL` are set. Two orderings still differ:
232
+ strings compare by the database collation on PostgreSQL and by code point
233
+ on MongoDB and DocumentDB, and arrays or objects at the sort path order
234
+ among themselves only on PostgreSQL (the other adapters fall back to id).
235
+ The legacy `IntegrationMappingRepository` inherits the port's
236
+ "not supported by this database adapter yet" error. Every adapter refuses
237
+ to run while field-level encryption still encrypts
238
+ `IntegrationMapping.mapping` on write (see `database/encryption/README.md`),
239
+ and returns rows decrypted like `findMappingsByIntegration`. PostgreSQL and
240
+ MongoDB decrypt through `decryptQueriedMappings`
241
+ (`database/encryption/integration-mapping-encryption.js`), a no-op while
242
+ encryption is off or lists no `IntegrationMapping` field besides `mapping`.
243
+ Validation lives in `integration-mapping-query.js`; a new operator is one
244
+ entry in its `OPERATORS` table, one in the Postgres adapter's
245
+ `CONDITION_SQL` and one in `CONDITION_EXPRESSIONS` in
246
+ `integration-mapping-query-pipeline.js`, the aggregation stages both
247
+ MongoDB-protocol adapters share. Those stages may only use what Amazon
248
+ DocumentDB 4.0 and 5.0 support (no `$facet`, `$getField`, `$set` or
249
+ `$unset`).
250
+ **Cost**: PostgreSQL answers in one SQL statement, which reads every row
251
+ of the integration and evaluates the JSON paths per row, because no JSON
252
+ index exists. With ~4 KB mappings on PostgreSQL 16 that is about 0.3 s per
253
+ call at 10⁴ rows per integration and 2.5–3 s at 10⁵. MongoDB answers in one
254
+ aggregate: the `integrationId` index narrows `$match` to the integration,
255
+ the `$expr` is then evaluated per document, and `$facet` returns the page
256
+ with its `$count`, so a page (after `omit`) must fit the 16 MB document
257
+ limit. DocumentDB has no `$facet`: the page and the count are two
258
+ concurrent aggregates, so the total does not come from the same snapshot
259
+ as the page. Both sort in memory on a computed key (`allowDiskUse`). With
260
+ ~4 KB mappings on MongoDB 7 that is about 0.05–0.1 s per call at 10⁴ rows
261
+ per integration and 0.5–0.7 s at 10⁵ (the DocumentDB pipeline, run on
262
+ MongoDB, 0.6–0.8 s). On every adapter a deep offset or an empty page past
263
+ the end costs about the same as the first page.
215
264
  - `process-repository-*.js` - Process (long-running job) persistence.
216
265
  Implements `applyProcessUpdate(processId, ops)` — a race-safe alternative
217
266
  to `update(id, patch)` that routes increments, sets, and bounded-array
@@ -414,6 +463,43 @@ const decrypted = await cryptor.decrypt(encrypted);
414
463
  - `integration-event-dispatcher.js` - Routes events to integration handlers
415
464
  - Supports lifecycle events and user actions
416
465
 
466
+ **Queue handler delivery**: a handler dispatched from the integration queue
467
+ (any `this.events` entry reached through SQS, including `ON_WEBHOOK` and
468
+ scheduled jobs) receives `{ data, context, delivery }`. HTTP-dispatched
469
+ handlers (`{ req, res, next }`) and `this.on` events do not.
470
+
471
+ | Field | Type | Value |
472
+ |---|---|---|
473
+ | `delivery.receiveCount` | `number \| undefined` | SQS `ApproximateReceiveCount` of this delivery |
474
+ | `delivery.maxReceiveCount` | `number \| undefined` | Receives allowed before SQS moves the message to the DLQ (`FRIGG_QUEUE_MAX_RECEIVE_COUNT`) |
475
+ | `delivery.isLastAttempt` | `boolean` | `true` only when both counts are known and `receiveCount >= maxReceiveCount` |
476
+
477
+ `isLastAttempt` is `false` when either count is unknown: a local or non-SQS
478
+ invocation, or a queue whose redrive policy the stack does not own
479
+ (`ownership.queue: 'external'`). The value is information only: core still
480
+ rethrows retryable errors and discards halt errors (4xx except 408/429).
481
+
482
+ Use it to end a run or count lost work on the final try: when a retryable
483
+ error (429, 5xx, network) is about to be rethrown and `isLastAttempt` is
484
+ `true`, the message goes to the DLQ next, so mark the run failed or count the
485
+ message's records as failed before rethrowing.
486
+
487
+ ```javascript
488
+ async processBatch({ data, delivery }) {
489
+ try {
490
+ await this.syncPage(data);
491
+ } catch (error) {
492
+ if (delivery?.isLastAttempt) await this.failRun(data.processId, error);
493
+ throw error;
494
+ }
495
+ }
496
+ ```
497
+
498
+ The single source of the max receive count is
499
+ `INTEGRATION_QUEUE_MAX_RECEIVE_COUNT` in `queues/queue-delivery.js`. The
500
+ devtools integration builder uses it for the queue's `RedrivePolicy` and sets
501
+ it as `FRIGG_QUEUE_MAX_RECEIVE_COUNT` on the queue worker function.
502
+
417
503
  ### 8. Error Handling (`/errors`)
418
504
 
419
505
  **Purpose**: Standardized error types with proper HTTP semantics.
@@ -437,22 +523,24 @@ if (!userId) {
437
523
 
438
524
  ### 9. Logging System (`/logs`)
439
525
 
440
- **Purpose**: Structured logging with debug capabilities.
526
+ **Purpose**: One redacted JSON record per line to stdout (ADR-048). See `docs/guides/LOGGING.md`.
441
527
 
442
- **Functions**:
443
- - `debug(message, data)` - Debug logging
444
- - `initDebugLog(eventName, event)` - Initialize debug context
445
- - `flushDebugLog(error)` - Flush logs on error
528
+ **Rules**:
529
+ - `logs/` is a leaf: it requires only Node built-ins and sibling `logs/` files.
530
+ - Integrations and API modules use `this.logger`; core uses one `getLogger('frigg.<area>')` per module.
531
+ - Fixed message text; ids and counts go into fields. `frigg.*` records at WARN and above need an `eventName` (`<logger name>.<action>`); tests fail without it.
532
+ - Log an error one time, at the boundary (`createHandler`, the express middleware, `Worker.run`, the DLQ processor). Inner code throws with `cause`.
533
+ - Use `statusCode`, not `status` (reserved key). `body`, `payload`, `response` are dropped at INFO and above.
534
+ - Tests assert on `createMemorySink()` records by `eventName`, not on console spies.
535
+ - `debug`, `initDebugLog`, `flushDebugLog` are deprecated shims.
446
536
 
447
537
  **Usage**:
448
538
 
449
539
  ```javascript
450
- const { debug, initDebugLog, flushDebugLog } = require('@friggframework/core');
540
+ const { getLogger } = require('../logs');
541
+ const log = getLogger('frigg.core.sync');
451
542
 
452
- initDebugLog('MyIntegration', event);
453
- debug('Processing request', { userId, action });
454
- // ... your code ...
455
- flushDebugLog(); // On error
543
+ log.warn('Sync skipped', { eventName: 'frigg.core.sync.skipped', processId });
456
544
  ```
457
545
 
458
546
  ### 10. Lambda Utilities (`/lambda`)
@@ -660,8 +748,8 @@ Use test doubles from `@friggframework/test` package for consistent mocking.
660
748
  ### Optional
661
749
 
662
750
  - `SECRET_ARN` - AWS Secrets Manager ARN for auto-injection
663
- - `DEBUG` - Debug logging pattern
664
- - `LOG_LEVEL` - Logging level (debug, info, warn, error)
751
+ - `FRIGG_LOG_LEVEL` - Minimum log level (`TRACE` … `FATAL`, default `INFO`; `DEBUG` on local runs)
752
+ - `FRIGG_QUEUE_MAX_RECEIVE_COUNT` - Set by devtools on integration queue workers whose queue the stack owns; feeds `delivery.maxReceiveCount`
665
753
 
666
754
  ## Version Information
667
755
 
package/README.md CHANGED
@@ -107,10 +107,6 @@ FRIGG_ENCRYPTION_KEY=your-256-bit-encryption-key
107
107
  AWS_REGION=us-east-1
108
108
  AWS_ACCESS_KEY_ID=your-access-key
109
109
  AWS_SECRET_ACCESS_KEY=your-secret-key
110
-
111
- # Logging
112
- DEBUG=frigg:*
113
- LOG_LEVEL=info
114
110
  ```
115
111
 
116
112
  ## Core Components
@@ -255,10 +251,12 @@ const {
255
251
  // Custom business logic error
256
252
  throw new RequiredPropertyError('userId is required');
257
253
 
258
- // API communication error
259
- throw new FetchError('Failed to fetch data from external API', {
260
- statusCode: 404,
261
- response: errorResponse
254
+ // API communication error. The message is built from the method, the
255
+ // sanitized URL and the status; the response body stays off the message.
256
+ throw await FetchError.create({
257
+ resource: 'https://api.example.com/contacts',
258
+ init: { method: 'GET' },
259
+ response,
262
260
  });
263
261
 
264
262
  // Base error with custom properties
@@ -270,25 +268,25 @@ throw new BaseError('Integration failed', {
270
268
 
271
269
  ### 6. Logging (`/logs`)
272
270
 
273
- Structured logging with debug capabilities.
271
+ One redacted JSON record per line to stdout, with the correlation ids of
272
+ ADR-011. See the [Logging guide](../../docs/guides/LOGGING.md).
274
273
 
275
274
  **Usage:**
276
275
  ```javascript
277
- const { debug, initDebugLog, flushDebugLog } = require('@friggframework/core');
278
-
279
- // Initialize debug logging
280
- initDebugLog('integration:slack');
281
-
282
- // Log debug information
283
- debug('Processing webhook payload', {
284
- eventType: 'contact.created',
285
- payload: webhookData
276
+ // In an integration or an API module
277
+ this.logger.info('Contact batch started', {
278
+ eventName: 'integration.hubspot.batch_started',
279
+ batchSize: contacts.length,
286
280
  });
287
281
 
288
- // Flush logs (useful in serverless environments)
289
- await flushDebugLog();
282
+ // In core code
283
+ const { getLogger } = require('@friggframework/core');
284
+ const log = getLogger('frigg.core.sync');
285
+ log.warn('Sync skipped', { eventName: 'frigg.core.sync.skipped', processId });
290
286
  ```
291
287
 
288
+ `debug`, `initDebugLog` and `flushDebugLog` are deprecated shims.
289
+
292
290
  ### 7. User Management (`/user`)
293
291
 
294
292
  Comprehensive user authentication and authorization system supporting both individual and organizational users.
@@ -1013,8 +1011,7 @@ const {
1013
1011
  | `MONGO_URI` | Yes | MongoDB connection string |
1014
1012
  | `FRIGG_ENCRYPTION_KEY` | Yes | 256-bit encryption key |
1015
1013
  | `AWS_REGION` | No | AWS region for services |
1016
- | `DEBUG` | No | Debug logging pattern |
1017
- | `LOG_LEVEL` | No | Logging level (debug, info, warn, error) |
1014
+ | `FRIGG_LOG_LEVEL` | No | Minimum log level: `TRACE`, `DEBUG`, `INFO` (default), `WARN`, `ERROR`, `FATAL`. Local runs default to `DEBUG` |
1018
1015
 
1019
1016
  ## License
1020
1017
 
package/core/CLAUDE.md CHANGED
@@ -21,13 +21,13 @@ This file provides guidance to Claude Code when working with the Frigg Framework
21
21
  - **Database Connection Management**: Automatic MongoDB connection with pooling
22
22
  - **Secrets Management**: AWS Secrets Manager integration via `SECRET_ARN` env var
23
23
  - **Error Sanitization**: Prevents internal details from leaking to end users
24
- - **Debug Logging**: Request/response logging with structured debug info
24
+ - **Invocation Scope**: `runInvocationScope` puts `requestId`, `route` and the redacted event summary on every record, then flushes usage, telemetry and sinks
25
25
  - **Connection Optimization**: `context.callbackWaitsForEmptyEventLoop = false` for reuse
26
26
 
27
27
  **Handler Configuration Options**:
28
28
  ```javascript
29
29
  const handler = createHandler({
30
- eventName: 'MyIntegration', // For logging/debugging
30
+ eventName: 'MyIntegration', // Logged as handlerName
31
31
  isUserFacingResponse: true, // true = sanitize errors, false = pass through
32
32
  method: async (event, context) => {}, // Your Lambda function logic
33
33
  shouldUseDatabase: true // false = skip MongoDB connection
@@ -52,9 +52,10 @@ const handler = createHandler({
52
52
  **Usage Pattern**:
53
53
  ```javascript
54
54
  class MyWorker extends Worker {
55
- async _run(params, context = {}) {
55
+ async _run(params, context = {}, delivery) {
56
56
  // Your job processing logic here
57
57
  // params are already JSON.parsed from SQS message body
58
+ // delivery is { receiveCount, maxReceiveCount, isLastAttempt }
58
59
  }
59
60
 
60
61
  _validateParams(params) {
@@ -129,7 +130,8 @@ class MyIntegration extends Delegate {
129
130
  ### Lambda Handler Lifecycle
130
131
  1. **Pre-Execution Setup**:
131
132
  ```javascript
132
- initDebugLog(eventName, event); // Debug logging setup
133
+ runInvocationScope({ requestId, handlerName, method, route, invocation }, …); // Logger scope for the invocation
134
+ log.info('Handler invoked', { eventName: 'frigg.handler.invoked' });
133
135
  await secretsToEnv(); // Secrets Manager injection
134
136
  await parametersToEnv(); // SSM Parameter Store fetch (only when SSM_PARAMETER_PREFIX + FRIGG_SSM_OFFLOADED_KEYS are set)
135
137
  context.callbackWaitsForEmptyEventLoop = false; // Connection pooling
@@ -149,12 +151,21 @@ class MyIntegration extends Delegate {
149
151
 
150
152
  4. **Error Handling & Cleanup**:
151
153
  ```javascript
152
- flushDebugLog(error); // Debug info flush on error
153
- // Sanitized error response for user-facing endpoints
154
+ // One record at the boundary: WARN frigg.handler.rejected (client-safe),
155
+ // ERROR frigg.handler.failed, or ERROR frigg.handler.halted (halt, no retry).
156
+ // Sanitized error response for user-facing endpoints; server-to-server
157
+ // errors are rethrown as a sanitized surrogate (toSanitizedSurrogate).
158
+ ```
159
+
160
+ 5. **Flush** (in `runInvocationScope`'s `finally`):
161
+ ```javascript
162
+ // Telemetry + log sinks under one deadline, fixed at flush start:
163
+ // min(flushTimeoutMs, remaining time - 50 ms). The usage rollup runs in
164
+ // parallel with no bound of its own.
154
165
  ```
155
166
 
156
167
  ### SQS Job Processing Lifecycle
157
- 1. **Batch Processing**: Process all records in `event.Records` sequentially
168
+ 1. **Batch Processing**: Process all records in `event.Records` sequentially, each inside `runMessageScope` (adds `messageId`, `receiveCount`, `processId`, `integrationId`, `integrationEvent` to its records)
158
169
  2. **Message Parsing**: JSON.parse message body for parameters
159
170
  3. **Validation**: Run custom validation on parsed parameters
160
171
  4. **Execution**: Call `_run()` method with validated parameters
@@ -205,7 +216,9 @@ const handler = createHandler({
205
216
  - Logs full error details internally
206
217
 
207
218
  2. **Server-to-Server Errors**: `isUserFacingResponse: false`
208
- - Re-throws original error for AWS handling
219
+ - Logs one ERROR, then rethrows a sanitized surrogate for AWS handling:
220
+ a fresh `Error` with the sanitized `name`, `message`, `stack`, plus
221
+ `statusCode` and `code`. `instanceof` checks and custom properties are gone
209
222
  - Used for SQS, SNS, and internal API calls
210
223
  - Enables proper retry mechanisms
211
224
 
@@ -214,12 +227,14 @@ const handler = createHandler({
214
227
  - Prevents infinite retries for known issues
215
228
  - Used for graceful degradation scenarios
216
229
 
217
- ### Debug Logging Strategy
230
+ ### Logging Strategy
218
231
  ```javascript
219
- initDebugLog(eventName, event); // Start logging context
220
- // ... your code ...
221
- flushDebugLog(error); // Flush on error (includes full context)
232
+ const { getLogger } = require('../logs');
233
+ const log = getLogger('frigg.core.sync');
234
+ log.info('Sync started', { eventName: 'frigg.core.sync.started', processId });
235
+ // Throw with cause; the boundary logs the error one time.
222
236
  ```
237
+ See `docs/guides/LOGGING.md`.
223
238
 
224
239
  ## Integration Development Patterns
225
240
 
@@ -646,7 +661,7 @@ describe('Health Handler', () => {
646
661
  const { createHandler } = require('@friggframework/core/core');
647
662
 
648
663
  const testHandler = createHandler({
649
- isUserFacingResponse: false, // Get full errors in tests
664
+ isUserFacingResponse: false, // Rethrows a sanitized surrogate (name, message, statusCode, code; not the original instance)
650
665
  shouldUseDatabase: false, // Mock/skip DB in tests
651
666
  method: yourTestMethod
652
667
  });
package/core/Worker.js CHANGED
@@ -2,8 +2,12 @@ const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/
2
2
  const _ = require('lodash');
3
3
  const { RequiredPropertyError } = require('../errors');
4
4
  const { get } = require('../assertions');
5
+ const { readQueueDelivery } = require('../queues/queue-delivery');
6
+ const { runMessageScope } = require('./invocation-scope');
7
+ const { getLogger } = require('../logs');
5
8
 
6
9
  const sqs = new SQSClient({ region: process.env.AWS_REGION });
10
+ const log = getLogger('frigg.worker');
7
11
 
8
12
  class Worker {
9
13
  async getQueueURL(params) {
@@ -25,45 +29,36 @@ class Worker {
25
29
  );
26
30
 
27
31
  for (const record of records) {
28
- // Log record entry with SQS-provided attributes useful for tracing
29
- // delivery history (ApproximateReceiveCount for retries, etc.).
30
- let parsedEvent;
31
- try {
32
- parsedEvent = JSON.parse(record.body)?.event;
33
- } catch {
34
- parsedEvent = undefined;
35
- }
36
- console.log(`[Worker] record begin`, {
37
- messageId: record.messageId,
38
- event: parsedEvent,
39
- receiveCount: record.attributes?.ApproximateReceiveCount,
40
- });
41
-
42
- try {
43
- const runParams = JSON.parse(record.body);
44
- this._validateParams(runParams);
45
- await this._run(runParams, context);
46
- console.log(`[Worker] record success`, {
47
- messageId: record.messageId,
48
- event: runParams?.event,
49
- });
50
- } catch (error) {
51
- if (error.isHaltError) {
52
- // HaltError means "discard this message, don't retry".
53
- // Treat as success so SQS deletes it from the queue.
54
- // Logged explicitly — silent discards made prod debugging
55
- // extremely hard; keep this visible.
56
- console.warn(`[Worker] record halted (discarded, no retry)`, {
57
- messageId: record.messageId,
58
- event: parsedEvent,
59
- reason: error.message,
60
- statusCode: error.statusCode,
32
+ await runMessageScope(record, async () => {
33
+ // messageId, receiveCount and the event come from the scope.
34
+ log.debug('Record started', { eventName: 'frigg.worker.record_started' });
35
+
36
+ try {
37
+ const runParams = JSON.parse(record.body);
38
+ this._validateParams(runParams);
39
+ await this._run(runParams, context, readQueueDelivery(record));
40
+ log.debug('Record succeeded', { eventName: 'frigg.worker.record_succeeded' });
41
+ } catch (error) {
42
+ if (error.isHaltError) {
43
+ // HaltError means "discard this message, don't retry".
44
+ // Treat as success so SQS deletes it from the queue.
45
+ // Logged explicitly — silent discards made prod debugging
46
+ // extremely hard; keep this visible.
47
+ log.error('Record halted (discarded, no retry)', {
48
+ eventName: 'frigg.worker.record_halted',
49
+ statusCode: error.statusCode,
50
+ error,
51
+ });
52
+ return;
53
+ }
54
+ // The message goes back to SQS, so WARN (ADR-048 §4).
55
+ log.warn('Record failed, returned for retry', {
56
+ eventName: 'frigg.worker.record_failed',
57
+ error,
61
58
  });
62
- continue;
59
+ batchItemFailures.push({ itemIdentifier: record.messageId });
63
60
  }
64
- console.error(`[Worker] Failed to process record ${record.messageId}:`, error);
65
- batchItemFailures.push({ itemIdentifier: record.messageId });
66
- }
61
+ });
67
62
  }
68
63
 
69
64
  if (batchItemFailures.length > 0) {