fg-tracker-sdk 0.10.0 → 0.13.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 (74) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +129 -35
  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 +47 -10
  14. package/dist/fetch.js.map +1 -1
  15. package/dist/genai/bedrock.d.ts +22 -0
  16. package/dist/genai/bedrock.d.ts.map +1 -0
  17. package/dist/genai/bedrock.js +161 -0
  18. package/dist/genai/bedrock.js.map +1 -0
  19. package/dist/genai/call.d.ts +96 -0
  20. package/dist/genai/call.d.ts.map +1 -0
  21. package/dist/genai/call.js +260 -0
  22. package/dist/genai/call.js.map +1 -0
  23. package/dist/genai/config.d.ts +71 -0
  24. package/dist/genai/config.d.ts.map +1 -0
  25. package/dist/genai/config.js +110 -0
  26. package/dist/genai/config.js.map +1 -0
  27. package/dist/genai/http.d.ts +49 -0
  28. package/dist/genai/http.d.ts.map +1 -0
  29. package/dist/genai/http.js +795 -0
  30. package/dist/genai/http.js.map +1 -0
  31. package/dist/genai/semconv.d.ts +64 -0
  32. package/dist/genai/semconv.d.ts.map +1 -0
  33. package/dist/genai/semconv.js +52 -0
  34. package/dist/genai/semconv.js.map +1 -0
  35. package/dist/genai/sse.d.ts +26 -0
  36. package/dist/genai/sse.d.ts.map +1 -0
  37. package/dist/genai/sse.js +100 -0
  38. package/dist/genai/sse.js.map +1 -0
  39. package/dist/index.d.ts +7 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +11 -3
  42. package/dist/index.js.map +1 -1
  43. package/dist/infra/aws.d.ts +45 -3
  44. package/dist/infra/aws.d.ts.map +1 -1
  45. package/dist/infra/aws.js +455 -29
  46. package/dist/infra/aws.js.map +1 -1
  47. package/dist/infra/lambda.d.ts +19 -0
  48. package/dist/infra/lambda.d.ts.map +1 -1
  49. package/dist/infra/lambda.js +61 -9
  50. package/dist/infra/lambda.js.map +1 -1
  51. package/dist/infra/messaging.d.ts +133 -0
  52. package/dist/infra/messaging.d.ts.map +1 -0
  53. package/dist/infra/messaging.js +366 -0
  54. package/dist/infra/messaging.js.map +1 -0
  55. package/dist/next.js +2 -2
  56. package/dist/next.js.map +1 -1
  57. package/dist/node-http.d.ts.map +1 -1
  58. package/dist/node-http.js +7 -3
  59. package/dist/node-http.js.map +1 -1
  60. package/dist/normalize.d.ts +32 -0
  61. package/dist/normalize.d.ts.map +1 -1
  62. package/dist/normalize.js +165 -10
  63. package/dist/normalize.js.map +1 -1
  64. package/dist/pg.d.ts +8 -3
  65. package/dist/pg.d.ts.map +1 -1
  66. package/dist/pg.js +512 -41
  67. package/dist/pg.js.map +1 -1
  68. package/dist/tracker.d.ts +17 -1
  69. package/dist/tracker.d.ts.map +1 -1
  70. package/dist/tracker.js +50 -5
  71. package/dist/tracker.js.map +1 -1
  72. package/dist/types.d.ts +26 -4
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +7 -1
package/dist/infra/aws.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { isBedrockRuntime, startBedrockCall } from '../genai/bedrock.js';
3
+ 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
4
  const PATCHED = Symbol.for('fg-tracker.aws.patched');
2
5
  const SERVICE_IDS = {
3
6
  sqs: 'sqs',
@@ -360,13 +363,205 @@ function httpStatus(value) {
360
363
  const status = obj(obj(value).$metadata).httpStatusCode;
361
364
  return typeof status === 'number' ? status : null;
362
365
  }
366
+ const received = new WeakMap();
367
+ /** Mensagens recebidas por clients instrumentados deste tracker (contexto lido antes de retirar os atributos). */
368
+ export function receivedMessages(core) {
369
+ let store = received.get(core);
370
+ if (!store) {
371
+ store = new ReceivedMessages();
372
+ received.set(core, store);
373
+ }
374
+ return store;
375
+ }
376
+ /**
377
+ * Fila FIFO pelo nome (o sufixo `.fifo` é do QueueName): último segmento do caminho da URL (com ou
378
+ * sem barra final, query ou fragmento) ou do ARN. Na dúvida (URL que não dá para ler), o nome cru.
379
+ */
380
+ export function isFifoQueue(queue) {
381
+ let name = queue;
382
+ if (queue.startsWith('arn:'))
383
+ name = queue.split(':').pop() ?? queue;
384
+ else {
385
+ try {
386
+ name = new URL(queue).pathname;
387
+ }
388
+ catch {
389
+ name = queue.split(/[?#]/)[0] ?? queue;
390
+ }
391
+ name = name.split('/').filter(Boolean).pop() ?? name;
392
+ }
393
+ return name.endsWith('.fifo');
394
+ }
395
+ function sqsPublishPlan(args, operation, ref, configured) {
396
+ const input = obj(args.input);
397
+ const queueName = ref?.resourceName ?? str(input.QueueUrl) ?? 'queue';
398
+ // FIFO: reenviar uma entrada recusada depois das aceitas mudaria a ordem do grupo. Sem o limite
399
+ // da fila declarado, o contexto só entra onde nenhuma fila o recusa (o mínimo do SQS, 1 KiB),
400
+ // e entradas de lote recusadas não são reenviadas fora de ordem.
401
+ const fifo = isFifoQueue(str(input.QueueUrl) ?? queueName);
402
+ const limits = { messageBytes: fifo && !configured.declared ? SQS_MIN_MESSAGE_BYTES : configured.messageBytes };
403
+ const base = {
404
+ 'messaging.system': 'aws_sqs',
405
+ 'messaging.operation.type': 'send',
406
+ 'messaging.operation.name': operation,
407
+ 'messaging.destination.name': queueName,
408
+ ...(ref?.resourceArn ? { 'aws.resourceArn': ref.resourceArn } : {}),
409
+ };
410
+ if (operation === 'SendMessage') {
411
+ return {
412
+ name: `SQS publish ${queueName}`,
413
+ attributes: base,
414
+ inject(ctx) {
415
+ const result = injectSqsContext(input, ctx, { messageBytes: limits.messageBytes, budgetBytes: Number.POSITIVE_INFINITY });
416
+ return {
417
+ args: result.injected ? { ...args, input: result.entry } : args,
418
+ injected: new Set(result.injected ? ['message'] : []),
419
+ skipped: result.skipped && result.skipped !== 'present' ? [result.skipped] : [],
420
+ };
421
+ },
422
+ restore(output, injected) {
423
+ if (!injected.has('message'))
424
+ return output;
425
+ const md5 = sqsAttributesMd5(input.MessageAttributes);
426
+ const out = { ...output };
427
+ if (md5)
428
+ out.MD5OfMessageAttributes = md5;
429
+ else
430
+ delete out.MD5OfMessageAttributes;
431
+ return out;
432
+ },
433
+ };
434
+ }
435
+ if (operation === 'SendMessageBatch') {
436
+ const entries = arr(input.Entries).map(obj);
437
+ const originals = new Map(entries.map((entry) => [String(entry.Id), entry]));
438
+ return {
439
+ name: `SQS publish ${queueName}`,
440
+ attributes: { ...base, 'messaging.batch.message_count': entries.length },
441
+ inject(ctx) {
442
+ // Orçamento agregado: a soma das entradas (atributos incluídos) também tem teto.
443
+ let remaining = SQS_MAX_BATCH_BYTES - entries.reduce((sum, entry) => sum + sqsMessageBytes(entry.MessageBody, entry.MessageAttributes), 0);
444
+ const injected = new Set();
445
+ const skipped = [];
446
+ const next = entries.map((entry) => {
447
+ const result = injectSqsContext(entry, ctx, { messageBytes: limits.messageBytes, budgetBytes: Math.max(0, remaining) });
448
+ if (result.injected) {
449
+ remaining -= result.addedBytes;
450
+ injected.add(String(entry.Id));
451
+ }
452
+ else if (result.skipped && result.skipped !== 'present')
453
+ skipped.push(result.skipped);
454
+ return result.entry;
455
+ });
456
+ return { args: injected.size > 0 ? { ...args, input: { ...input, Entries: next } } : args, injected, skipped };
457
+ },
458
+ restore(output, injected) {
459
+ if (injected.size === 0 || !Array.isArray(output.Successful))
460
+ return output;
461
+ return {
462
+ ...output,
463
+ Successful: output.Successful.map((item) => {
464
+ const ok = obj(item);
465
+ const id = String(ok.Id);
466
+ if (!injected.has(id))
467
+ return item;
468
+ const md5 = sqsAttributesMd5(originals.get(id)?.MessageAttributes);
469
+ const out = { ...ok };
470
+ if (md5)
471
+ out.MD5OfMessageAttributes = md5;
472
+ else
473
+ delete out.MD5OfMessageAttributes;
474
+ return out;
475
+ }),
476
+ };
477
+ },
478
+ retryEntries(output, injected) {
479
+ if (fifo)
480
+ return null;
481
+ const failed = arr(output.Failed).map(obj).filter((item) => injected.has(String(item.Id)) && item.SenderFault === true && isValidationCode(item.Code));
482
+ if (failed.length === 0)
483
+ return null;
484
+ const retry = failed.map((item) => originals.get(String(item.Id))).filter(Boolean);
485
+ return {
486
+ args: { ...args, input: { ...input, Entries: retry } },
487
+ merge(first, retried) {
488
+ const retriedIds = new Set([...arr(retried.Successful), ...arr(retried.Failed)].map((item) => String(obj(item).Id)));
489
+ return {
490
+ ...first,
491
+ Successful: [...arr(first.Successful), ...arr(retried.Successful)],
492
+ Failed: [...arr(first.Failed).filter((item) => !retriedIds.has(String(obj(item).Id))), ...arr(retried.Failed)],
493
+ };
494
+ },
495
+ };
496
+ },
497
+ };
498
+ }
499
+ return null;
500
+ }
501
+ function eventBridgePublishPlan(args, limits) {
502
+ const input = obj(args.input);
503
+ const entries = arr(input.Entries).map(obj);
504
+ if (entries.length === 0)
505
+ return null;
506
+ const bus = str(entries[0].EventBusName) ?? 'default';
507
+ const busName = bus.startsWith('arn:') ? bus.split('/').pop() || bus : bus;
508
+ return {
509
+ name: `EventBridge publish ${busName}`,
510
+ attributes: {
511
+ 'messaging.system': 'aws_eventbridge',
512
+ 'messaging.operation.type': 'send',
513
+ 'messaging.operation.name': 'PutEvents',
514
+ 'messaging.destination.name': busName,
515
+ 'messaging.batch.message_count': entries.length,
516
+ },
517
+ inject(ctx) {
518
+ // "Menor que" o limite: sobra pelo menos 1 byte.
519
+ let remaining = limits.requestBytes - 1 - entries.reduce((sum, entry) => sum + eventBridgeEntryBytes(entry), 0);
520
+ const injected = new Set();
521
+ const skipped = [];
522
+ const next = entries.map((entry, index) => {
523
+ const result = injectEventBridgeContext(entry, ctx, Math.max(0, remaining));
524
+ if (result.injected) {
525
+ remaining -= result.addedBytes;
526
+ injected.add(String(index));
527
+ }
528
+ else if (result.skipped && result.skipped !== 'present')
529
+ skipped.push(result.skipped);
530
+ return result.entry;
531
+ });
532
+ return { args: injected.size > 0 ? { ...args, input: { ...input, Entries: next } } : args, injected, skipped };
533
+ },
534
+ restore: (output) => output,
535
+ retryEntries(output, injected) {
536
+ const results = arr(output.Entries).map(obj);
537
+ const indexes = results.flatMap((result, index) => (injected.has(String(index)) && isValidationCode(result.ErrorCode) ? [index] : []));
538
+ if (indexes.length === 0)
539
+ return null;
540
+ return {
541
+ args: { ...args, input: { ...input, Entries: indexes.map((index) => entries[index]) } },
542
+ // Resultados por posição: troca só as posições reenviadas.
543
+ merge(first, retried) {
544
+ const merged = arr(first.Entries).slice();
545
+ const again = arr(retried.Entries);
546
+ indexes.forEach((index, position) => {
547
+ if (again[position] !== undefined)
548
+ merged[index] = again[position];
549
+ });
550
+ return { ...first, Entries: merged, FailedEntryCount: merged.filter((item) => obj(item).ErrorCode).length };
551
+ },
552
+ };
553
+ },
554
+ };
555
+ }
363
556
  /**
364
557
  * Adiciona o middleware `fgTrackerInfra` (step `initialize`) ao client.
365
558
  * - Agregados de infra (§27.4) para SQS, SNS, EventBridge, Kinesis, DynamoDB e Step Functions — nunca amostrados.
366
559
  * - Span `http.client` por chamada (qualquer serviço) quando há um trace em andamento; respeita amostragem.
367
560
  * - DynamoDB: injeta `ReturnConsumedCapacity: 'TOTAL'` quando não informado.
561
+ * - SQS/EventBridge (§16.1): span `custom` `SQS publish <fila>`/`EventBridge publish <barramento>` com o
562
+ * contexto propagado nas mensagens; `ReceiveMessage` pede o contexto e o guarda para o `trackMessage`.
368
563
  */
369
- export function instrumentAwsClient(core, client) {
564
+ export function instrumentAwsClient(core, client, options = {}) {
370
565
  const target = client;
371
566
  if (!core.enabled || target[PATCHED])
372
567
  return client;
@@ -375,12 +570,19 @@ export function instrumentAwsClient(core, client) {
375
570
  return client;
376
571
  }
377
572
  target[PATCHED] = true;
573
+ const propagation = options.propagation ?? core.options.propagateTraceTo !== false;
574
+ const sqsLimits = {
575
+ messageBytes: Math.min(SQS_MAX_MESSAGE_BYTES, Math.max(SQS_MIN_MESSAGE_BYTES, options.sqsMaxMessageBytes ?? SQS_MAX_MESSAGE_BYTES)),
576
+ declared: options.sqsMaxMessageBytes !== undefined,
577
+ };
578
+ const eventBridgeLimits = { requestBytes: Math.min(EVENTBRIDGE_MAX_REQUEST_BYTES, Math.max(1024, options.eventBridgeMaxRequestBytes ?? EVENTBRIDGE_MAX_REQUEST_BYTES)) };
378
579
  let regionPromise = null;
379
580
  let endpointPromise = null;
380
581
  const middleware = (next, context) => async (args) => {
381
582
  if (!core.enabled)
382
583
  return next(args);
383
584
  const { service, label } = detectService(client, context);
585
+ const bedrock = isBedrockRuntime(label);
384
586
  const operation = (context?.commandName ?? 'Unknown').replace(/Command$/, '');
385
587
  let info = {};
386
588
  let region;
@@ -401,22 +603,6 @@ export function instrumentAwsClient(core, client) {
401
603
  catch {
402
604
  info = {};
403
605
  }
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
606
  const safeRecord = (output, error) => {
421
607
  try {
422
608
  info.record?.(output, error);
@@ -425,24 +611,222 @@ export function instrumentAwsClient(core, client) {
425
611
  /* agregação nunca quebra a chamada */
426
612
  }
427
613
  };
614
+ /** A chamada em si, com o span `http.client` quando há trace. Não registra agregados. */
615
+ /**
616
+ * `injected`: tentativa com o contexto que a instrumentação acrescentou. Se o serviço a recusa
617
+ * por validação, a culpa é da injeção (a chamada é repetida sem ela): o span registra a recusa
618
+ * sem `error`, para não virar issue da aplicação.
619
+ */
620
+ const call = async (finalArgs, decorate, injected = false) => {
621
+ const parent = core.currentContext();
622
+ const resource = info.primary;
623
+ const startedPerf = performance.now();
624
+ const span = parent
625
+ ? core.startSpan('http.client', `AWS ${label} ${operation}${resource?.resourceName ? ` ${resource.resourceName}` : ''}`, {
626
+ 'aws.service': label,
627
+ 'aws.operation': operation,
628
+ ...(resource?.resourceArn ? { 'aws.resourceArn': resource.resourceArn } : {}),
629
+ ...(resource?.resourceName ? { 'aws.resourceName': resource.resourceName } : {}),
630
+ ...(region ? { 'aws.region': region } : {}),
631
+ ...(endpointHost ? { 'server.address': endpointHost, host: endpointHost } : {}),
632
+ })
633
+ : null;
634
+ if (span) {
635
+ span.method = 'POST';
636
+ span.route = `/${label}/${operation}`;
637
+ }
638
+ // Bedrock Converse/ConverseStream: atributos GenAI no próprio span; o fim do span passa a ser do observador.
639
+ let genAi = null;
640
+ const genAiConfig = span && bedrock ? core.genAiFor(span) : null;
641
+ if (span && genAiConfig) {
642
+ try {
643
+ genAi = startBedrockCall(genAiConfig, span, operation, finalArgs?.input, startedPerf);
644
+ }
645
+ catch {
646
+ genAi = null;
647
+ }
648
+ }
649
+ let handedOff = false;
650
+ try {
651
+ const result = span ? await core.runInSpan(span, () => next(finalArgs)) : await next(finalArgs);
652
+ const output = obj(result?.output);
653
+ if (span) {
654
+ span.statusCode = httpStatus(output) ?? 200;
655
+ try {
656
+ decorate?.(span, output);
657
+ }
658
+ catch {
659
+ /* atributos são best-effort */
660
+ }
661
+ }
662
+ if (genAi && span) {
663
+ handedOff = true;
664
+ try {
665
+ genAi.output(result);
666
+ }
667
+ catch {
668
+ span.end();
669
+ }
670
+ }
671
+ return result;
672
+ }
673
+ catch (err) {
674
+ if (span) {
675
+ span.statusCode = httpStatus(err);
676
+ if (genAi) {
677
+ handedOff = true;
678
+ try {
679
+ genAi.error(err);
680
+ }
681
+ catch {
682
+ span.end();
683
+ }
684
+ }
685
+ else if (injected && isValidationError(err)) {
686
+ span.setAttribute('messaging.propagation.rejected', true);
687
+ const name = err?.name;
688
+ if (typeof name === 'string')
689
+ span.setAttribute('error.type', name);
690
+ }
691
+ else {
692
+ span.setError(err);
693
+ }
694
+ }
695
+ throw err;
696
+ }
697
+ finally {
698
+ // Com observador GenAI, quem fecha é ele (já fechou, ou fecha no fim do stream).
699
+ if (!handedOff)
700
+ span?.end();
701
+ }
702
+ };
703
+ // ReceiveMessage: pede o contexto, guarda-o por mensagem e devolve a resposta como o chamador pediu.
704
+ if (propagation && service === 'sqs' && operation === 'ReceiveMessage') {
705
+ let plan = null;
706
+ try {
707
+ plan = planReceive(obj(callArgs.input));
708
+ }
709
+ catch {
710
+ plan = null;
711
+ }
712
+ if (plan) {
713
+ const store = receivedMessages(core);
714
+ try {
715
+ // Os produtores das mensagens recebidas viram links do span da chamada (antes de ele fechar).
716
+ const result = await call({ ...callArgs, input: plan.input }, (span, output) => {
717
+ const contexts = arr(output.Messages).map((message) => metaContext(readSqsMessage(message)));
718
+ const { links, dropped } = uniqueLinks(contexts);
719
+ if (links.length > 0)
720
+ span.setAttribute('links', links);
721
+ if (dropped > 0)
722
+ span.setAttribute('links.dropped', dropped);
723
+ });
724
+ const output = obj(result?.output);
725
+ let finalResult = result;
726
+ if (Array.isArray(output.Messages)) {
727
+ const messages = output.Messages.map((message) => {
728
+ const record = obj(message);
729
+ try {
730
+ store.remember(record, readSqsMessage(record));
731
+ return stripReceivedMessage(record, plan);
732
+ }
733
+ catch {
734
+ return message;
735
+ }
736
+ });
737
+ finalResult = { ...result, output: { ...output, Messages: messages } };
738
+ }
739
+ safeRecord(obj(finalResult.output), null);
740
+ return finalResult;
741
+ }
742
+ catch (err) {
743
+ safeRecord(null, err);
744
+ throw err;
745
+ }
746
+ }
747
+ }
748
+ let publish = null;
749
+ if (propagation && core.currentContext()) {
750
+ try {
751
+ if (service === 'sqs')
752
+ publish = sqsPublishPlan(callArgs, operation, info.primary, sqsLimits);
753
+ else if (service === 'eventbridge' && operation === 'PutEvents')
754
+ publish = eventBridgePublishPlan(callArgs, eventBridgeLimits);
755
+ }
756
+ catch {
757
+ publish = null;
758
+ }
759
+ }
760
+ if (!publish) {
761
+ try {
762
+ const result = await call(callArgs);
763
+ safeRecord(obj(result?.output), null);
764
+ return result;
765
+ }
766
+ catch (err) {
767
+ safeRecord(null, err);
768
+ throw err;
769
+ }
770
+ }
771
+ const plan = publish;
772
+ const publishSpan = core.startSpan('custom', plan.name, { ...plan.attributes, ...(region ? { 'aws.region': region } : {}) });
428
773
  try {
429
- const result = span ? await core.runInSpan(span, () => next(callArgs)) : await next(callArgs);
774
+ const result = await core.runInSpan(publishSpan, async () => {
775
+ let injection;
776
+ try {
777
+ injection = plan.inject(publishSpan.context());
778
+ }
779
+ catch {
780
+ injection = { args: callArgs, injected: new Set(), skipped: [] };
781
+ }
782
+ publishSpan.setAttribute('messaging.propagation.injected', injection.injected.size);
783
+ if (injection.skipped.length > 0) {
784
+ publishSpan.setAttribute('messaging.propagation.skipped', injection.skipped.length);
785
+ publishSpan.setAttribute('messaging.propagation.skip_reasons', [...new Set(injection.skipped)]);
786
+ }
787
+ let res;
788
+ try {
789
+ res = await call(injection.args, undefined, injection.injected.size > 0);
790
+ }
791
+ catch (err) {
792
+ // Recusa de validação com o contexto: nada foi publicado; repete como o usuário montou.
793
+ if (injection.injected.size === 0 || !isValidationError(err))
794
+ throw err;
795
+ publishSpan.setAttribute('messaging.propagation.fallback', 'request');
796
+ const plain = await call(callArgs);
797
+ return plain;
798
+ }
799
+ let output = obj(res?.output);
800
+ try {
801
+ const retry = plan.retryEntries?.(output, injection.injected);
802
+ if (retry) {
803
+ publishSpan.setAttribute('messaging.propagation.fallback', 'entries');
804
+ // Falha no reenvio: fica a resposta original, com as entradas recusadas.
805
+ const again = await call(retry.args).then((value) => obj(value?.output), () => null);
806
+ if (again)
807
+ output = retry.merge(output, again);
808
+ }
809
+ output = plan.restore(output, injection.injected);
810
+ }
811
+ catch {
812
+ /* a resposta original continua valendo */
813
+ }
814
+ return { ...res, output };
815
+ });
430
816
  const output = obj(result?.output);
431
817
  safeRecord(output, null);
432
- if (span)
433
- span.statusCode = httpStatus(output) ?? 200;
818
+ const messageId = str(output.MessageId);
819
+ if (messageId)
820
+ publishSpan.setAttribute('messaging.message.id', messageId);
434
821
  return result;
435
822
  }
436
823
  catch (err) {
437
824
  safeRecord(null, err);
438
- if (span) {
439
- span.statusCode = httpStatus(err);
440
- span.setError(err);
441
- }
825
+ publishSpan.setError(err);
442
826
  throw err;
443
827
  }
444
828
  finally {
445
- span?.end();
829
+ publishSpan.end();
446
830
  }
447
831
  };
448
832
  client.middlewareStack.add(middleware, { step: 'initialize', name: 'fgTrackerInfra', override: true, tags: ['FG_TRACKER'] });
@@ -450,18 +834,60 @@ export function instrumentAwsClient(core, client) {
450
834
  }
451
835
  /**
452
836
  * 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.
837
+ * e cria um span `custom` `SQS process <fila>` para os spans internos (AWS, db, fetch) formarem um trace.
838
+ *
839
+ * Com `record`, o span continua o trace do produtor: raiz local com `remoteParentSpanId` (ou filho
840
+ * do span atual quando ele já está no mesmo trace, como no `wrapLambda` de uma mensagem só). Cada
841
+ * chamada tem o próprio contexto, inclusive em `Promise.all`; o contexto da invocação vira link.
454
842
  */
455
- export async function trackQueueMessage(core, queue, fn) {
843
+ export async function trackQueueMessage(core, queue, fn, record) {
456
844
  if (!core.enabled)
457
845
  return fn();
458
846
  const ref = sqsQueueRef(queue, process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || undefined);
459
847
  core.infra?.learnAccount(ref.resourceArn);
460
- const span = core.startSpan('custom', `SQS process ${ref.resourceName ?? queue}`, {
848
+ let meta;
849
+ if (record) {
850
+ try {
851
+ const own = readSqsMessage(record);
852
+ const stashed = receivedMessages(core).lookup(record);
853
+ meta = {
854
+ traceparent: own.traceparent ?? stashed?.traceparent,
855
+ tracestate: own.tracestate ?? stashed?.tracestate,
856
+ sentTimestamp: own.sentTimestamp ?? stashed?.sentTimestamp,
857
+ receiveCount: own.receiveCount ?? stashed?.receiveCount,
858
+ messageId: own.messageId ?? stashed?.messageId,
859
+ };
860
+ }
861
+ catch {
862
+ meta = undefined;
863
+ }
864
+ }
865
+ const attributes = {
461
866
  'messaging.system': 'aws_sqs',
867
+ 'messaging.operation.type': 'process',
868
+ 'messaging.destination.name': ref.resourceName ?? queue,
462
869
  ...(ref.resourceArn ? { 'aws.resourceArn': ref.resourceArn } : {}),
463
870
  ...(ref.region ? { 'aws.region': ref.region } : {}),
464
- });
871
+ };
872
+ if (meta?.messageId)
873
+ attributes['messaging.message.id'] = meta.messageId;
874
+ if (meta?.sentTimestamp !== undefined)
875
+ attributes['messaging.message.receive.latency'] = Math.max(0, Date.now() - meta.sentTimestamp);
876
+ if (meta?.receiveCount !== undefined)
877
+ attributes['messaging.message.retry.count'] = Math.max(0, meta.receiveCount - 1);
878
+ const remote = metaContext(meta);
879
+ const current = core.currentContext();
880
+ const name = `SQS process ${ref.resourceName ?? queue}`;
881
+ let span;
882
+ if (remote && current?.traceId !== remote.traceId) {
883
+ // Lote de mensagens de produtores diferentes: a invocação fica no próprio trace, como link.
884
+ if (current)
885
+ attributes.links = [{ traceId: current.traceId, spanId: current.spanId }];
886
+ span = core.startRemoteRoot('custom', name, attributes, remote);
887
+ }
888
+ else {
889
+ span = core.startSpan('custom', name, attributes);
890
+ }
465
891
  const started = performance.now();
466
892
  let failed = false;
467
893
  try {