fg-tracker-sdk 0.10.0 → 0.11.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +53 -3
  3. package/dist/context.d.ts +9 -0
  4. package/dist/context.d.ts.map +1 -1
  5. package/dist/context.js +27 -0
  6. package/dist/context.js.map +1 -1
  7. package/dist/database.d.ts.map +1 -1
  8. package/dist/database.js +34 -5
  9. package/dist/database.js.map +1 -1
  10. package/dist/express.js +2 -2
  11. package/dist/express.js.map +1 -1
  12. package/dist/fetch.d.ts.map +1 -1
  13. package/dist/fetch.js +16 -8
  14. package/dist/fetch.js.map +1 -1
  15. package/dist/index.d.ts +4 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +10 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/infra/aws.d.ts +45 -3
  20. package/dist/infra/aws.d.ts.map +1 -1
  21. package/dist/infra/aws.js +419 -29
  22. package/dist/infra/aws.js.map +1 -1
  23. package/dist/infra/lambda.d.ts +19 -0
  24. package/dist/infra/lambda.d.ts.map +1 -1
  25. package/dist/infra/lambda.js +61 -9
  26. package/dist/infra/lambda.js.map +1 -1
  27. package/dist/infra/messaging.d.ts +133 -0
  28. package/dist/infra/messaging.d.ts.map +1 -0
  29. package/dist/infra/messaging.js +366 -0
  30. package/dist/infra/messaging.js.map +1 -0
  31. package/dist/next.js +2 -2
  32. package/dist/next.js.map +1 -1
  33. package/dist/node-http.d.ts.map +1 -1
  34. package/dist/node-http.js +7 -3
  35. package/dist/node-http.js.map +1 -1
  36. package/dist/normalize.d.ts +32 -0
  37. package/dist/normalize.d.ts.map +1 -1
  38. package/dist/normalize.js +165 -10
  39. package/dist/normalize.js.map +1 -1
  40. package/dist/pg.d.ts +8 -3
  41. package/dist/pg.d.ts.map +1 -1
  42. package/dist/pg.js +512 -41
  43. package/dist/pg.js.map +1 -1
  44. package/dist/tracker.d.ts +8 -1
  45. package/dist/tracker.d.ts.map +1 -1
  46. package/dist/tracker.js +36 -5
  47. package/dist/tracker.js.map +1 -1
  48. package/dist/types.d.ts +17 -4
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +4 -1
@@ -1,5 +1,6 @@
1
1
  import type { TrackerCore } from '../tracker.js';
2
2
  import type { InfraResourceRef } from './aggregator.js';
3
+ import { ReceivedMessages } from './messaging.js';
3
4
  /**
4
5
  * Instrumentação do AWS SDK v3 por middleware (CONTRATO.md §27.8).
5
6
  * Tipos estruturais: o SDK não depende de `@aws-sdk/*`.
@@ -36,16 +37,57 @@ export declare function parseArn(arn: string): {
36
37
  * Também aceita um ARN pronto.
37
38
  */
38
39
  export declare function sqsQueueRef(queue: string, fallbackRegion?: string): InfraResourceRef;
40
+ export interface AwsInstrumentationOptions {
41
+ /**
42
+ * Propaga o contexto do trace nas mensagens: `traceparent`/`tracestate` em `MessageAttributes`
43
+ * (SQS `SendMessage`/`SendMessageBatch`) e no `Detail` (EventBridge `PutEvents`), e pede esses
44
+ * atributos no `ReceiveMessage`. Default `true` (salvo `propagateTraceTo: false` no tracker).
45
+ */
46
+ propagation?: boolean;
47
+ /**
48
+ * Limite de bytes por mensagem SQS usado para decidir se o contexto cabe. Default 1 MiB (cota do
49
+ * SQS). Use o `MaximumMessageSize` da fila se ele for menor; mesmo sem isso, numa fila padrão,
50
+ * uma recusa por tamanho com o contexto é repetida sem ele. Em fila FIFO (`.fifo`), sem esta
51
+ * opção o contexto só vai em mensagens de até 1 KiB (o menor `MaximumMessageSize` possível):
52
+ * reenviar uma entrada recusada do lote depois das aceitas mudaria a ordem do grupo.
53
+ */
54
+ sqsMaxMessageBytes?: number;
55
+ /** Limite do pedido `PutEvents` (soma das entradas). Default 1 MiB. */
56
+ eventBridgeMaxRequestBytes?: number;
57
+ }
58
+ /** Mensagens recebidas por clients instrumentados deste tracker (contexto lido antes de retirar os atributos). */
59
+ export declare function receivedMessages(core: TrackerCore): ReceivedMessages;
60
+ /**
61
+ * Fila FIFO pelo nome (o sufixo `.fifo` é do QueueName): último segmento do caminho da URL (com ou
62
+ * sem barra final, query ou fragmento) ou do ARN. Na dúvida (URL que não dá para ler), o nome cru.
63
+ */
64
+ export declare function isFifoQueue(queue: string): boolean;
39
65
  /**
40
66
  * Adiciona o middleware `fgTrackerInfra` (step `initialize`) ao client.
41
67
  * - Agregados de infra (§27.4) para SQS, SNS, EventBridge, Kinesis, DynamoDB e Step Functions — nunca amostrados.
42
68
  * - Span `http.client` por chamada (qualquer serviço) quando há um trace em andamento; respeita amostragem.
43
69
  * - DynamoDB: injeta `ReturnConsumedCapacity: 'TOTAL'` quando não informado.
70
+ * - SQS/EventBridge (§16.1): span `custom` `SQS publish <fila>`/`EventBridge publish <barramento>` com o
71
+ * contexto propagado nas mensagens; `ReceiveMessage` pede o contexto e o guarda para o `trackMessage`.
44
72
  */
45
- export declare function instrumentAwsClient<T extends AwsClientLike>(core: TrackerCore, client: T): T;
73
+ export declare function instrumentAwsClient<T extends AwsClientLike>(core: TrackerCore, client: T, options?: AwsInstrumentationOptions): T;
74
+ /** Forma da mensagem aceita pelo `trackMessage`: record do evento da Lambda ou `Message` do AWS SDK. */
75
+ export type QueueMessageLike = object;
76
+ export interface TrackMessageOptions {
77
+ /**
78
+ * A mensagem processada (record do evento SQS da Lambda ou `Message` do `ReceiveMessage`): o
79
+ * processamento continua o trace do produtor (pai remoto da própria mensagem), com o atraso de
80
+ * fila e as tentativas. Sem ela, o processamento é medido só localmente.
81
+ */
82
+ record?: QueueMessageLike;
83
+ }
46
84
  /**
47
85
  * Mede o processamento de uma mensagem consumida (`processingMsSum`, `processingMsMax`, `errors`)
48
- * e cria um span `custom` para os spans internos (AWS, db, fetch) formarem um trace.
86
+ * e cria um span `custom` `SQS process <fila>` para os spans internos (AWS, db, fetch) formarem um trace.
87
+ *
88
+ * Com `record`, o span continua o trace do produtor: raiz local com `remoteParentSpanId` (ou filho
89
+ * do span atual quando ele já está no mesmo trace, como no `wrapLambda` de uma mensagem só). Cada
90
+ * chamada tem o próprio contexto, inclusive em `Promise.all`; o contexto da invocação vira link.
49
91
  */
50
- export declare function trackQueueMessage<T>(core: TrackerCore, queue: string, fn: () => T | Promise<T>): Promise<T>;
92
+ export declare function trackQueueMessage<T>(core: TrackerCore, queue: string, fn: () => T | Promise<T>, record?: QueueMessageLike): Promise<T>;
51
93
  //# sourceMappingURL=aws.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"aws.d.ts","sourceRoot":"","sources":["../../src/infra/aws.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAA8B,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEpF;;;GAGG;AAEH,iGAAiG;AACjG,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE;QACf,GAAG,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,iBAAiB,KAAK,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC/H,CAAC;IACF,MAAM,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7E;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE5J,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AA2ED,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAIlG;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAgBpF;AAuPD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,aAAa,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CA+E5F;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA0BjH"}
1
+ {"version":3,"file":"aws.d.ts","sourceRoot":"","sources":["../../src/infra/aws.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,KAAK,EAA8B,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAUL,gBAAgB,EAWjB,MAAM,gBAAgB,CAAC;AAExB;;;GAGG;AAEH,iGAAiG;AACjG,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE;QACf,GAAG,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,iBAAiB,KAAK,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC/H,CAAC;IACF,MAAM,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7E;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE5J,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AA2ED,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAIlG;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAgBpF;AAuPD,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,uEAAuE;IACvE,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAID,kHAAkH;AAClH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,gBAAgB,CAOpE;AAoBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAYlD;AAqJD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,aAAa,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,GAAE,yBAA8B,GAAG,CAAC,CA0NrI;AAED,wGAAwG;AACxG,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEtC,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,CAAC,CA0D5I"}
package/dist/infra/aws.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { EVENTBRIDGE_MAX_REQUEST_BYTES, eventBridgeEntryBytes, injectEventBridgeContext, injectSqsContext, isValidationCode, isValidationError, metaContext, planReceive, readSqsMessage, ReceivedMessages, SQS_MAX_BATCH_BYTES, SQS_MAX_MESSAGE_BYTES, SQS_MIN_MESSAGE_BYTES, sqsAttributesMd5, sqsMessageBytes, stripReceivedMessage, uniqueLinks, } from './messaging.js';
1
2
  const PATCHED = Symbol.for('fg-tracker.aws.patched');
2
3
  const SERVICE_IDS = {
3
4
  sqs: 'sqs',
@@ -360,13 +361,205 @@ function httpStatus(value) {
360
361
  const status = obj(obj(value).$metadata).httpStatusCode;
361
362
  return typeof status === 'number' ? status : null;
362
363
  }
364
+ const received = new WeakMap();
365
+ /** Mensagens recebidas por clients instrumentados deste tracker (contexto lido antes de retirar os atributos). */
366
+ export function receivedMessages(core) {
367
+ let store = received.get(core);
368
+ if (!store) {
369
+ store = new ReceivedMessages();
370
+ received.set(core, store);
371
+ }
372
+ return store;
373
+ }
374
+ /**
375
+ * Fila FIFO pelo nome (o sufixo `.fifo` é do QueueName): último segmento do caminho da URL (com ou
376
+ * sem barra final, query ou fragmento) ou do ARN. Na dúvida (URL que não dá para ler), o nome cru.
377
+ */
378
+ export function isFifoQueue(queue) {
379
+ let name = queue;
380
+ if (queue.startsWith('arn:'))
381
+ name = queue.split(':').pop() ?? queue;
382
+ else {
383
+ try {
384
+ name = new URL(queue).pathname;
385
+ }
386
+ catch {
387
+ name = queue.split(/[?#]/)[0] ?? queue;
388
+ }
389
+ name = name.split('/').filter(Boolean).pop() ?? name;
390
+ }
391
+ return name.endsWith('.fifo');
392
+ }
393
+ function sqsPublishPlan(args, operation, ref, configured) {
394
+ const input = obj(args.input);
395
+ const queueName = ref?.resourceName ?? str(input.QueueUrl) ?? 'queue';
396
+ // FIFO: reenviar uma entrada recusada depois das aceitas mudaria a ordem do grupo. Sem o limite
397
+ // da fila declarado, o contexto só entra onde nenhuma fila o recusa (o mínimo do SQS, 1 KiB),
398
+ // e entradas de lote recusadas não são reenviadas fora de ordem.
399
+ const fifo = isFifoQueue(str(input.QueueUrl) ?? queueName);
400
+ const limits = { messageBytes: fifo && !configured.declared ? SQS_MIN_MESSAGE_BYTES : configured.messageBytes };
401
+ const base = {
402
+ 'messaging.system': 'aws_sqs',
403
+ 'messaging.operation.type': 'send',
404
+ 'messaging.operation.name': operation,
405
+ 'messaging.destination.name': queueName,
406
+ ...(ref?.resourceArn ? { 'aws.resourceArn': ref.resourceArn } : {}),
407
+ };
408
+ if (operation === 'SendMessage') {
409
+ return {
410
+ name: `SQS publish ${queueName}`,
411
+ attributes: base,
412
+ inject(ctx) {
413
+ const result = injectSqsContext(input, ctx, { messageBytes: limits.messageBytes, budgetBytes: Number.POSITIVE_INFINITY });
414
+ return {
415
+ args: result.injected ? { ...args, input: result.entry } : args,
416
+ injected: new Set(result.injected ? ['message'] : []),
417
+ skipped: result.skipped && result.skipped !== 'present' ? [result.skipped] : [],
418
+ };
419
+ },
420
+ restore(output, injected) {
421
+ if (!injected.has('message'))
422
+ return output;
423
+ const md5 = sqsAttributesMd5(input.MessageAttributes);
424
+ const out = { ...output };
425
+ if (md5)
426
+ out.MD5OfMessageAttributes = md5;
427
+ else
428
+ delete out.MD5OfMessageAttributes;
429
+ return out;
430
+ },
431
+ };
432
+ }
433
+ if (operation === 'SendMessageBatch') {
434
+ const entries = arr(input.Entries).map(obj);
435
+ const originals = new Map(entries.map((entry) => [String(entry.Id), entry]));
436
+ return {
437
+ name: `SQS publish ${queueName}`,
438
+ attributes: { ...base, 'messaging.batch.message_count': entries.length },
439
+ inject(ctx) {
440
+ // Orçamento agregado: a soma das entradas (atributos incluídos) também tem teto.
441
+ let remaining = SQS_MAX_BATCH_BYTES - entries.reduce((sum, entry) => sum + sqsMessageBytes(entry.MessageBody, entry.MessageAttributes), 0);
442
+ const injected = new Set();
443
+ const skipped = [];
444
+ const next = entries.map((entry) => {
445
+ const result = injectSqsContext(entry, ctx, { messageBytes: limits.messageBytes, budgetBytes: Math.max(0, remaining) });
446
+ if (result.injected) {
447
+ remaining -= result.addedBytes;
448
+ injected.add(String(entry.Id));
449
+ }
450
+ else if (result.skipped && result.skipped !== 'present')
451
+ skipped.push(result.skipped);
452
+ return result.entry;
453
+ });
454
+ return { args: injected.size > 0 ? { ...args, input: { ...input, Entries: next } } : args, injected, skipped };
455
+ },
456
+ restore(output, injected) {
457
+ if (injected.size === 0 || !Array.isArray(output.Successful))
458
+ return output;
459
+ return {
460
+ ...output,
461
+ Successful: output.Successful.map((item) => {
462
+ const ok = obj(item);
463
+ const id = String(ok.Id);
464
+ if (!injected.has(id))
465
+ return item;
466
+ const md5 = sqsAttributesMd5(originals.get(id)?.MessageAttributes);
467
+ const out = { ...ok };
468
+ if (md5)
469
+ out.MD5OfMessageAttributes = md5;
470
+ else
471
+ delete out.MD5OfMessageAttributes;
472
+ return out;
473
+ }),
474
+ };
475
+ },
476
+ retryEntries(output, injected) {
477
+ if (fifo)
478
+ return null;
479
+ const failed = arr(output.Failed).map(obj).filter((item) => injected.has(String(item.Id)) && item.SenderFault === true && isValidationCode(item.Code));
480
+ if (failed.length === 0)
481
+ return null;
482
+ const retry = failed.map((item) => originals.get(String(item.Id))).filter(Boolean);
483
+ return {
484
+ args: { ...args, input: { ...input, Entries: retry } },
485
+ merge(first, retried) {
486
+ const retriedIds = new Set([...arr(retried.Successful), ...arr(retried.Failed)].map((item) => String(obj(item).Id)));
487
+ return {
488
+ ...first,
489
+ Successful: [...arr(first.Successful), ...arr(retried.Successful)],
490
+ Failed: [...arr(first.Failed).filter((item) => !retriedIds.has(String(obj(item).Id))), ...arr(retried.Failed)],
491
+ };
492
+ },
493
+ };
494
+ },
495
+ };
496
+ }
497
+ return null;
498
+ }
499
+ function eventBridgePublishPlan(args, limits) {
500
+ const input = obj(args.input);
501
+ const entries = arr(input.Entries).map(obj);
502
+ if (entries.length === 0)
503
+ return null;
504
+ const bus = str(entries[0].EventBusName) ?? 'default';
505
+ const busName = bus.startsWith('arn:') ? bus.split('/').pop() || bus : bus;
506
+ return {
507
+ name: `EventBridge publish ${busName}`,
508
+ attributes: {
509
+ 'messaging.system': 'aws_eventbridge',
510
+ 'messaging.operation.type': 'send',
511
+ 'messaging.operation.name': 'PutEvents',
512
+ 'messaging.destination.name': busName,
513
+ 'messaging.batch.message_count': entries.length,
514
+ },
515
+ inject(ctx) {
516
+ // "Menor que" o limite: sobra pelo menos 1 byte.
517
+ let remaining = limits.requestBytes - 1 - entries.reduce((sum, entry) => sum + eventBridgeEntryBytes(entry), 0);
518
+ const injected = new Set();
519
+ const skipped = [];
520
+ const next = entries.map((entry, index) => {
521
+ const result = injectEventBridgeContext(entry, ctx, Math.max(0, remaining));
522
+ if (result.injected) {
523
+ remaining -= result.addedBytes;
524
+ injected.add(String(index));
525
+ }
526
+ else if (result.skipped && result.skipped !== 'present')
527
+ skipped.push(result.skipped);
528
+ return result.entry;
529
+ });
530
+ return { args: injected.size > 0 ? { ...args, input: { ...input, Entries: next } } : args, injected, skipped };
531
+ },
532
+ restore: (output) => output,
533
+ retryEntries(output, injected) {
534
+ const results = arr(output.Entries).map(obj);
535
+ const indexes = results.flatMap((result, index) => (injected.has(String(index)) && isValidationCode(result.ErrorCode) ? [index] : []));
536
+ if (indexes.length === 0)
537
+ return null;
538
+ return {
539
+ args: { ...args, input: { ...input, Entries: indexes.map((index) => entries[index]) } },
540
+ // Resultados por posição: troca só as posições reenviadas.
541
+ merge(first, retried) {
542
+ const merged = arr(first.Entries).slice();
543
+ const again = arr(retried.Entries);
544
+ indexes.forEach((index, position) => {
545
+ if (again[position] !== undefined)
546
+ merged[index] = again[position];
547
+ });
548
+ return { ...first, Entries: merged, FailedEntryCount: merged.filter((item) => obj(item).ErrorCode).length };
549
+ },
550
+ };
551
+ },
552
+ };
553
+ }
363
554
  /**
364
555
  * Adiciona o middleware `fgTrackerInfra` (step `initialize`) ao client.
365
556
  * - Agregados de infra (§27.4) para SQS, SNS, EventBridge, Kinesis, DynamoDB e Step Functions — nunca amostrados.
366
557
  * - Span `http.client` por chamada (qualquer serviço) quando há um trace em andamento; respeita amostragem.
367
558
  * - DynamoDB: injeta `ReturnConsumedCapacity: 'TOTAL'` quando não informado.
559
+ * - SQS/EventBridge (§16.1): span `custom` `SQS publish <fila>`/`EventBridge publish <barramento>` com o
560
+ * contexto propagado nas mensagens; `ReceiveMessage` pede o contexto e o guarda para o `trackMessage`.
368
561
  */
369
- export function instrumentAwsClient(core, client) {
562
+ export function instrumentAwsClient(core, client, options = {}) {
370
563
  const target = client;
371
564
  if (!core.enabled || target[PATCHED])
372
565
  return client;
@@ -375,6 +568,12 @@ export function instrumentAwsClient(core, client) {
375
568
  return client;
376
569
  }
377
570
  target[PATCHED] = true;
571
+ const propagation = options.propagation ?? core.options.propagateTraceTo !== false;
572
+ const sqsLimits = {
573
+ messageBytes: Math.min(SQS_MAX_MESSAGE_BYTES, Math.max(SQS_MIN_MESSAGE_BYTES, options.sqsMaxMessageBytes ?? SQS_MAX_MESSAGE_BYTES)),
574
+ declared: options.sqsMaxMessageBytes !== undefined,
575
+ };
576
+ const eventBridgeLimits = { requestBytes: Math.min(EVENTBRIDGE_MAX_REQUEST_BYTES, Math.max(1024, options.eventBridgeMaxRequestBytes ?? EVENTBRIDGE_MAX_REQUEST_BYTES)) };
378
577
  let regionPromise = null;
379
578
  let endpointPromise = null;
380
579
  const middleware = (next, context) => async (args) => {
@@ -401,22 +600,6 @@ export function instrumentAwsClient(core, client) {
401
600
  catch {
402
601
  info = {};
403
602
  }
404
- const parent = core.currentContext();
405
- const resource = info.primary;
406
- const span = parent
407
- ? core.startSpan('http.client', `AWS ${label} ${operation}${resource?.resourceName ? ` ${resource.resourceName}` : ''}`, {
408
- 'aws.service': label,
409
- 'aws.operation': operation,
410
- ...(resource?.resourceArn ? { 'aws.resourceArn': resource.resourceArn } : {}),
411
- ...(resource?.resourceName ? { 'aws.resourceName': resource.resourceName } : {}),
412
- ...(region ? { 'aws.region': region } : {}),
413
- ...(endpointHost ? { 'server.address': endpointHost, host: endpointHost } : {}),
414
- })
415
- : null;
416
- if (span) {
417
- span.method = 'POST';
418
- span.route = `/${label}/${operation}`;
419
- }
420
603
  const safeRecord = (output, error) => {
421
604
  try {
422
605
  info.record?.(output, error);
@@ -425,24 +608,189 @@ export function instrumentAwsClient(core, client) {
425
608
  /* agregação nunca quebra a chamada */
426
609
  }
427
610
  };
611
+ /** A chamada em si, com o span `http.client` quando há trace. Não registra agregados. */
612
+ /**
613
+ * `injected`: tentativa com o contexto que a instrumentação acrescentou. Se o serviço a recusa
614
+ * por validação, a culpa é da injeção (a chamada é repetida sem ela): o span registra a recusa
615
+ * sem `error`, para não virar issue da aplicação.
616
+ */
617
+ const call = async (finalArgs, decorate, injected = false) => {
618
+ const parent = core.currentContext();
619
+ const resource = info.primary;
620
+ const span = parent
621
+ ? core.startSpan('http.client', `AWS ${label} ${operation}${resource?.resourceName ? ` ${resource.resourceName}` : ''}`, {
622
+ 'aws.service': label,
623
+ 'aws.operation': operation,
624
+ ...(resource?.resourceArn ? { 'aws.resourceArn': resource.resourceArn } : {}),
625
+ ...(resource?.resourceName ? { 'aws.resourceName': resource.resourceName } : {}),
626
+ ...(region ? { 'aws.region': region } : {}),
627
+ ...(endpointHost ? { 'server.address': endpointHost, host: endpointHost } : {}),
628
+ })
629
+ : null;
630
+ if (span) {
631
+ span.method = 'POST';
632
+ span.route = `/${label}/${operation}`;
633
+ }
634
+ try {
635
+ const result = span ? await core.runInSpan(span, () => next(finalArgs)) : await next(finalArgs);
636
+ const output = obj(result?.output);
637
+ if (span) {
638
+ span.statusCode = httpStatus(output) ?? 200;
639
+ try {
640
+ decorate?.(span, output);
641
+ }
642
+ catch {
643
+ /* atributos são best-effort */
644
+ }
645
+ }
646
+ return result;
647
+ }
648
+ catch (err) {
649
+ if (span) {
650
+ span.statusCode = httpStatus(err);
651
+ if (injected && isValidationError(err)) {
652
+ span.setAttribute('messaging.propagation.rejected', true);
653
+ const name = err?.name;
654
+ if (typeof name === 'string')
655
+ span.setAttribute('error.type', name);
656
+ }
657
+ else {
658
+ span.setError(err);
659
+ }
660
+ }
661
+ throw err;
662
+ }
663
+ finally {
664
+ span?.end();
665
+ }
666
+ };
667
+ // ReceiveMessage: pede o contexto, guarda-o por mensagem e devolve a resposta como o chamador pediu.
668
+ if (propagation && service === 'sqs' && operation === 'ReceiveMessage') {
669
+ let plan = null;
670
+ try {
671
+ plan = planReceive(obj(callArgs.input));
672
+ }
673
+ catch {
674
+ plan = null;
675
+ }
676
+ if (plan) {
677
+ const store = receivedMessages(core);
678
+ try {
679
+ // Os produtores das mensagens recebidas viram links do span da chamada (antes de ele fechar).
680
+ const result = await call({ ...callArgs, input: plan.input }, (span, output) => {
681
+ const contexts = arr(output.Messages).map((message) => metaContext(readSqsMessage(message)));
682
+ const { links, dropped } = uniqueLinks(contexts);
683
+ if (links.length > 0)
684
+ span.setAttribute('links', links);
685
+ if (dropped > 0)
686
+ span.setAttribute('links.dropped', dropped);
687
+ });
688
+ const output = obj(result?.output);
689
+ let finalResult = result;
690
+ if (Array.isArray(output.Messages)) {
691
+ const messages = output.Messages.map((message) => {
692
+ const record = obj(message);
693
+ try {
694
+ store.remember(record, readSqsMessage(record));
695
+ return stripReceivedMessage(record, plan);
696
+ }
697
+ catch {
698
+ return message;
699
+ }
700
+ });
701
+ finalResult = { ...result, output: { ...output, Messages: messages } };
702
+ }
703
+ safeRecord(obj(finalResult.output), null);
704
+ return finalResult;
705
+ }
706
+ catch (err) {
707
+ safeRecord(null, err);
708
+ throw err;
709
+ }
710
+ }
711
+ }
712
+ let publish = null;
713
+ if (propagation && core.currentContext()) {
714
+ try {
715
+ if (service === 'sqs')
716
+ publish = sqsPublishPlan(callArgs, operation, info.primary, sqsLimits);
717
+ else if (service === 'eventbridge' && operation === 'PutEvents')
718
+ publish = eventBridgePublishPlan(callArgs, eventBridgeLimits);
719
+ }
720
+ catch {
721
+ publish = null;
722
+ }
723
+ }
724
+ if (!publish) {
725
+ try {
726
+ const result = await call(callArgs);
727
+ safeRecord(obj(result?.output), null);
728
+ return result;
729
+ }
730
+ catch (err) {
731
+ safeRecord(null, err);
732
+ throw err;
733
+ }
734
+ }
735
+ const plan = publish;
736
+ const publishSpan = core.startSpan('custom', plan.name, { ...plan.attributes, ...(region ? { 'aws.region': region } : {}) });
428
737
  try {
429
- const result = span ? await core.runInSpan(span, () => next(callArgs)) : await next(callArgs);
738
+ const result = await core.runInSpan(publishSpan, async () => {
739
+ let injection;
740
+ try {
741
+ injection = plan.inject(publishSpan.context());
742
+ }
743
+ catch {
744
+ injection = { args: callArgs, injected: new Set(), skipped: [] };
745
+ }
746
+ publishSpan.setAttribute('messaging.propagation.injected', injection.injected.size);
747
+ if (injection.skipped.length > 0) {
748
+ publishSpan.setAttribute('messaging.propagation.skipped', injection.skipped.length);
749
+ publishSpan.setAttribute('messaging.propagation.skip_reasons', [...new Set(injection.skipped)]);
750
+ }
751
+ let res;
752
+ try {
753
+ res = await call(injection.args, undefined, injection.injected.size > 0);
754
+ }
755
+ catch (err) {
756
+ // Recusa de validação com o contexto: nada foi publicado; repete como o usuário montou.
757
+ if (injection.injected.size === 0 || !isValidationError(err))
758
+ throw err;
759
+ publishSpan.setAttribute('messaging.propagation.fallback', 'request');
760
+ const plain = await call(callArgs);
761
+ return plain;
762
+ }
763
+ let output = obj(res?.output);
764
+ try {
765
+ const retry = plan.retryEntries?.(output, injection.injected);
766
+ if (retry) {
767
+ publishSpan.setAttribute('messaging.propagation.fallback', 'entries');
768
+ // Falha no reenvio: fica a resposta original, com as entradas recusadas.
769
+ const again = await call(retry.args).then((value) => obj(value?.output), () => null);
770
+ if (again)
771
+ output = retry.merge(output, again);
772
+ }
773
+ output = plan.restore(output, injection.injected);
774
+ }
775
+ catch {
776
+ /* a resposta original continua valendo */
777
+ }
778
+ return { ...res, output };
779
+ });
430
780
  const output = obj(result?.output);
431
781
  safeRecord(output, null);
432
- if (span)
433
- span.statusCode = httpStatus(output) ?? 200;
782
+ const messageId = str(output.MessageId);
783
+ if (messageId)
784
+ publishSpan.setAttribute('messaging.message.id', messageId);
434
785
  return result;
435
786
  }
436
787
  catch (err) {
437
788
  safeRecord(null, err);
438
- if (span) {
439
- span.statusCode = httpStatus(err);
440
- span.setError(err);
441
- }
789
+ publishSpan.setError(err);
442
790
  throw err;
443
791
  }
444
792
  finally {
445
- span?.end();
793
+ publishSpan.end();
446
794
  }
447
795
  };
448
796
  client.middlewareStack.add(middleware, { step: 'initialize', name: 'fgTrackerInfra', override: true, tags: ['FG_TRACKER'] });
@@ -450,18 +798,60 @@ export function instrumentAwsClient(core, client) {
450
798
  }
451
799
  /**
452
800
  * Mede o processamento de uma mensagem consumida (`processingMsSum`, `processingMsMax`, `errors`)
453
- * e cria um span `custom` para os spans internos (AWS, db, fetch) formarem um trace.
801
+ * e cria um span `custom` `SQS process <fila>` para os spans internos (AWS, db, fetch) formarem um trace.
802
+ *
803
+ * Com `record`, o span continua o trace do produtor: raiz local com `remoteParentSpanId` (ou filho
804
+ * do span atual quando ele já está no mesmo trace, como no `wrapLambda` de uma mensagem só). Cada
805
+ * chamada tem o próprio contexto, inclusive em `Promise.all`; o contexto da invocação vira link.
454
806
  */
455
- export async function trackQueueMessage(core, queue, fn) {
807
+ export async function trackQueueMessage(core, queue, fn, record) {
456
808
  if (!core.enabled)
457
809
  return fn();
458
810
  const ref = sqsQueueRef(queue, process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || undefined);
459
811
  core.infra?.learnAccount(ref.resourceArn);
460
- const span = core.startSpan('custom', `SQS process ${ref.resourceName ?? queue}`, {
812
+ let meta;
813
+ if (record) {
814
+ try {
815
+ const own = readSqsMessage(record);
816
+ const stashed = receivedMessages(core).lookup(record);
817
+ meta = {
818
+ traceparent: own.traceparent ?? stashed?.traceparent,
819
+ tracestate: own.tracestate ?? stashed?.tracestate,
820
+ sentTimestamp: own.sentTimestamp ?? stashed?.sentTimestamp,
821
+ receiveCount: own.receiveCount ?? stashed?.receiveCount,
822
+ messageId: own.messageId ?? stashed?.messageId,
823
+ };
824
+ }
825
+ catch {
826
+ meta = undefined;
827
+ }
828
+ }
829
+ const attributes = {
461
830
  'messaging.system': 'aws_sqs',
831
+ 'messaging.operation.type': 'process',
832
+ 'messaging.destination.name': ref.resourceName ?? queue,
462
833
  ...(ref.resourceArn ? { 'aws.resourceArn': ref.resourceArn } : {}),
463
834
  ...(ref.region ? { 'aws.region': ref.region } : {}),
464
- });
835
+ };
836
+ if (meta?.messageId)
837
+ attributes['messaging.message.id'] = meta.messageId;
838
+ if (meta?.sentTimestamp !== undefined)
839
+ attributes['messaging.message.receive.latency'] = Math.max(0, Date.now() - meta.sentTimestamp);
840
+ if (meta?.receiveCount !== undefined)
841
+ attributes['messaging.message.retry.count'] = Math.max(0, meta.receiveCount - 1);
842
+ const remote = metaContext(meta);
843
+ const current = core.currentContext();
844
+ const name = `SQS process ${ref.resourceName ?? queue}`;
845
+ let span;
846
+ if (remote && current?.traceId !== remote.traceId) {
847
+ // Lote de mensagens de produtores diferentes: a invocação fica no próprio trace, como link.
848
+ if (current)
849
+ attributes.links = [{ traceId: current.traceId, spanId: current.spanId }];
850
+ span = core.startRemoteRoot('custom', name, attributes, remote);
851
+ }
852
+ else {
853
+ span = core.startSpan('custom', name, attributes);
854
+ }
465
855
  const started = performance.now();
466
856
  let failed = false;
467
857
  try {