npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fg-tracker-sdk

v0.8.1

Published

SDK de instrumentação Node para o FG Track (Express, NestJS, Next.js, pg, fetch), tracing distribuído, releases, check-ins de jobs, envio de logs (pino, winston, NestJS Logger) e agregados de infraestrutura (AWS SDK v3, pool de conexões, ECS, Lambda)

Readme

fg-tracker-sdk

Instrumentação Node para o FG Track e envio de logs (pino, winston, NestJS Logger). Única dependência de runtime: pino-abstract-transport (a instrumentação AWS usa tipos estruturais, sem @aws-sdk/*); usa AsyncLocalStorage para encadear spans e o fetch global do Node para enviar lotes ao POST /v1/ingest.

Requisitos: Node ≥ 20 (recomendado 24). O pacote é ESM; em projetos CommonJS (NestJS) funciona com require() no Node ≥ 22.12 ou via import() dinâmico.

Instalação

pnpm add fg-tracker-sdk              # publicado no npm
# ou, dentro do workspace:
# "fg-tracker-sdk": "workspace:*"

Endpoint

O default de endpoint é http://localhost:4050 (constante DEFAULT_ENDPOINT, API local do monorepo). Em produção passe a URL da API mostrada no painel em Conectar (por exemplo via FG_TRACKER_URL). O endereço de produção da API é https://tracker-api.fortground.com.br (o antigo track-api.fortground.com.br continua respondendo).

API

import { createTracker } from 'fg-tracker-sdk';

const tracker = createTracker({
  apiKey: process.env.FG_TRACKER_KEY!,   // stk_...
  endpoint: 'http://localhost:4050',       // default
  serviceName: 'minha-app',
  flushIntervalMs: 2000,                   // default
});

app.use(tracker.express());                 // Express / NestJS (platform-express)
tracker.instrumentPg(pool);                 // pg.Pool → spans db.query com query normalizada
await tracker.span('custom', 'send-email', async (span) => { span.setAttribute('to', email); });
export const GET = tracker.wrapNextRoute(async (req) => Response.json({ ok: true }));
const fetchTracked = tracker.wrapFetch();   // fetch instrumentado → spans http.client
await tracker.flush(); await tracker.shutdown();

| opção | default | descrição | |---|---|---| | apiKey | — | chave do projeto; sem ela o tracker vira no-op (com aviso) | | endpoint | http://localhost:4050 | URL base da API | | serviceName | — | vai em attributes.serviceName de todo span | | flushIntervalMs | 2000 | intervalo de envio | | maxBatchSize | 200 | ≥ N spans na fila dispara envio imediato | | maxQueueSize | 5000 | acima disso descarta os spans mais antigos | | enabled | true | false desliga tudo sem tirar o código | | debug | false | loga cada envio | | release | detectada | versão em execução (FG_TRACKER_RELEASE, GITHUB_SHA, VERCEL_GIT_COMMIT_SHA… → ${serviceName}@${sha}); false desliga | | environment | detectado | FG_TRACKER_ENV, APP_ENV ou NODE_ENV; false desliga | | propagateTraceTo | true | destinos que recebem traceparent no fetch instrumentado (true, lista de hosts/prefixos/RegExp ou false) | | instrumentGlobalFetch | false | substitui globalThis.fetch pelo fetch instrumentado (provedores externos viram spans) | | instrumentNodeHttp | true | spans http.client para a saída de node:http/node:https (AWS SDK, axios, got, node-fetch 2, gaxios, web-push) | | autoSpanMinDurationMs | 5 | duração mínima para um trecho medido com measure/instrumentNest virar span | | measureEventLoopLag | true | mede o atraso do event loop na requisição e grava eventLoop.maxLagMs/eventLoop.blockedMs no span raiz quando o loop trava | | eventLoopLagThresholdMs | 50 | atraso mínimo para virar atributo | | captureGlobalErrors | false | uncaughtExceptionMonitor + unhandledRejection viram spans com erro | | tracesSampleRate | 1 | fração de traces enviados (decidida na raiz; filhos seguem; traceparent sai com -00 quando não amostrado) | | tracesSampler | — | (ctx: { kind, name, attributes, parentSampled? }) => taxa por operação (ex.: 0 para GET /health) | | ignoreTransactions | [] | nomes de raiz (string/RegExp) que nunca são enviados | | ignoreErrors | [] | erros (name: message, string/RegExp) que não viram issue (attributes.ignoredError = true) | | scrubFields | DEFAULT_SCRUB_FIELDS | chaves de atributos que viram [Filtered] (password, token, authorization, cookie, cpf…); false desliga | | beforeSend | — | (span) => span \| null, último filtro antes da fila | | infra | { enabled: true, idleIntervalMs: 60000 } | agregados de infraestrutura (veja Infraestrutura); nunca amostrados |

Erros, usuários, releases e jobs

app.use(tracker.express());                  // continua `traceparent` de outro serviço; associa req.user.id ao fim
app.use(routes);
app.use(tracker.expressErrorHandler());      // exceção real no span → issues agrupadas por causa, não por status

tracker.setUser({ id: user.id, email: user.email });     // impacto por usuário nas issues
tracker.captureException(err, { stage: 'checkout' });   // em filtros do Nest / catch de jobs
tracker.traceHeaders();                                  // { traceparent } para clientes HTTP não instrumentados
tracker.currentTraceId();                                // para logs / x-trace-id

await tracker.checkIn('backup-diario', () => runBackup(), { intervalSeconds: 86400, graceSeconds: 600 }); // monitor de job
const run = await tracker.monitor('sync').start(); /* ... */ await run.ok(); // ou run.fail(err)

await tracker.registerRelease({ commitSha: process.env.GITHUB_SHA, repository: 'FORTGROUND/phase' }); // ou o CLI:
// FG_TRACKER_KEY=stk_... npx fg-tracker-release --service minha-app --previous-sha $PREV --env production

O CLI fg-tracker-release lê git log/git show e envia commits, arquivos e intervalos de linhas alterados: o painel usa isso para apontar commits/PRs suspeitos em cada issue e comparar a latência de cada rota entre releases.

Jobs com crontab: tracker.checkIn('relatorio', run, { cron: '0 3 * * *', timezone: 'America/Sao_Paulo', maxRuntimeSeconds: 1800 }) — o monitor calcula o próximo disparo no fuso, marca timeout quando passa do tempo máximo e aceita failureThreshold/recoveryThreshold na tela.

Source maps do front (para o fg-tracker-browser): FG_TRACKER_KEY=stk_... npx fg-tracker-sourcemaps --dir dist --release web@$GITHUB_SHA --url-prefix ~/assets sobe os .map de uma release; a API traduz o stack minificado na ingestão.

Falhas de envio são logadas uma vez e o lote é reenviado no próximo flush (respostas 4xx descartam o lote — normalmente é chave inválida). O timer usa unref(), então não segura o processo vivo; chame shutdown() no encerramento para não perder os últimos spans.

Express

import express from 'express';
import { createTracker } from 'fg-tracker-sdk';

const tracker = createTracker({ apiKey: process.env.FG_TRACKER_KEY!, serviceName: 'minha-api' });
const app = express();
app.use(tracker.express());          // ANTES de qualquer outro middleware/rota
app.get('/users/:id', async (req, res) => { /* ... */ });

process.on('SIGTERM', () => tracker.shutdown().finally(() => process.exit(0)));

O middleware cria um span raiz http.server por request. O nome é ${method} ${route}; a rota vem de req.baseUrl + req.route.path (disponível no finish da resposta), com fallback para o path com uuids/números trocados por :id. Atributos: url, userAgent, ip. Status ≥ 500 vira error.

NestJS (platform-express)

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { createTracker } from 'fg-tracker-sdk';
import { AppModule } from './app.module';

export const tracker = createTracker({ apiKey: process.env.FG_TRACKER_KEY!, serviceName: 'minha-api' });

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(tracker.express());        // antes de helmet/cors/pipes
  app.enableShutdownHooks();
  await app.listen(4040);
}
bootstrap();

Para encerrar limpo, chame tracker.shutdown() num OnApplicationShutdown (ou no handler de SIGTERM).

Next.js (App Router)

// src/lib/tracker.ts
import { createTracker } from 'fg-tracker-sdk';
export const tracker = createTracker({ apiKey: process.env.FG_TRACKER_KEY!, serviceName: 'meu-site' });

// app/api/users/[id]/route.ts
import { tracker } from '@/lib/tracker';
export const GET = tracker.wrapNextRoute(async (req, ctx) => {
  const user = await db.query('select * from users where id = $1', [id]);
  return Response.json(user);
});

O span http.server recebe ${method} ${pathname normalizado} e o status da Response. Só funciona no runtime Node (não no Edge). Em output: 'export' não há route handlers dinâmicos, então nada a instrumentar.

pg

import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
tracker.instrumentPg(pool);

pool.query passa a gerar spans db.query apenas quando há um span pai no contexto (request em andamento ou tracker.span). O nome é o SQL normalizado — números → ?, strings → '?', IN (...) → IN (?), espaços colapsados, minúsculas, 200 chars — e attributes.rows recebe o rowCount. Todas as assinaturas de query são suportadas (promise ou callback). Queries repetidas ≥ loop_threshold vezes no mesmo request viram uma issue N_PLUS_ONE na API.

Se você usa drizzle/kysely/knex sobre pg.Pool, instrumente o pool antes de passá-lo ao ORM.

Espera de conexão

Num span db.query do postgres (porsager), o atributo db.poolWaitMs diz quanto daquele tempo foi esperando uma conexão, e não executando SQL.

Isso importa porque as duas causas têm soluções opostas: uma query de 2 s pode ser SQL ruim (falta índice) ou pool saturado (falta conexão). Sem separar, a duração sozinha não distingue — e otimizar o SQL não resolve pool cheio.

A medida é exata, não estimada: o driver marca a query como ativa no instante em que a conexão a assume, e é esse instante que é capturado. Só aparece acima de 1 ms; abaixo disso é ruído de agendamento.

Num pool frio a espera inclui estabelecer a conexão (TCP, TLS, autenticação). Esperar por uma conexão que ainda não existe é esperar por conexão do mesmo jeito, então isso é proposital.

fetch

const fetchTracked = tracker.wrapFetch();       // envolve globalThis.fetch
const res = await fetchTracked('https://api.github.com/repos/FORTGROUND/fg-tracker');

// ou globalmente (cuidado: afeta todo o processo)
globalThis.fetch = tracker.wrapFetch(globalThis.fetch);

Cada chamada vira um span http.client nomeado ${method} ${origin}${pathname normalizado} (ex.: GET https://api.github.com/repos/:id). É isso que alimenta a detecção de LOOP: a mesma chamada repetida ≥ loop_threshold vezes sob o mesmo pai. Chamadas ao próprio endpoint do FG Track são ignoradas.

node:http e node:https (automático)

O fetch cobre quem chama fetch. Todo o resto do ecossistema Node continua falando pelo módulo http, e por isso o SDK também instrumenta a saída de node:http/node:https — ligado por default, sem nenhuma linha de código:

| biblioteca | transporte | |---|---| | AWS SDK v3 (S3, DynamoDB, Lambda, SQS…) | @smithy/node-http-handler → node:https | | google-auth-library, googleapis | gaxios → node-fetch 2 → node:https | | axios, got, node-fetch 2, request | node:http/node:https | | web-push, @apple/app-store-server-library | node:https |

Vale para qualquer cliente, inclusive um que você nem sabe que existe dentro de uma dependência — e não depende de lembrar de envolver cada cliente. A implementação usa diagnostics_channel, o ponto de observação que o próprio Node publica: nada é substituído por monkeypatch, nada quebra se outra biblioteca instrumentar o mesmo módulo, e a ordem de import não importa.

As regras são as mesmas do fetch: span http.client com o mesmo nome e attributes.host, só dentro de um trace em andamento (um poller de background não vira trace órfão), traceparent propagado conforme propagateTraceTo e sem tocar num header que o chamador já definiu, 5xx marcando erro no span, e chamadas ao próprio endpoint do FG Track ignoradas.

Para desligar:

createTracker({ apiKey, serviceName: 'minha-app', instrumentNodeHttp: false });

Chamadas feitas direto pela API do undici (e não por fetch) não passam por node:http e não são cobertas; use tracker.wrapFetch() nesse caso.

Spans por método, automáticos (NestJS)

Instrumentar à mão só cobre o que alguém já suspeitava. O trecho que some do waterfall é justamente aquele em que ninguém pensou. Uma linha no bootstrap envolve todos os providers e controllers:

import { ModulesContainer } from '@nestjs/core';

const app = await NestFactory.create(AppModule);
tracker.instrumentNest(app.get(ModulesContainer));   // devolve quantos métodos envolveu

Cada método vira Classe.metodo, e só aparece no trace se passar de autoSpanMinDurationMs (default 5 ms). Método trivial chamado mil vezes por requisição não deixa rastro; o método que segurou 1,6 s aparece com nome e duração. Erro sempre vira span, mesmo rápido.

O container é lido estruturalmente: o SDK não importa @nestjs/core, não ganha peer dependency e não fica preso a uma versão do Nest. Providers do próprio framework (Reflector, ApplicationConfig, …) são pulados — o Nest os re-registra em todo módulo, e medir o framework não diz nada sobre a latência de uma rota. Hooks de ciclo de vida (onModuleInit e companhia) também ficam de fora, porque rodam no boot.

tracker.instrumentNest(container, {
  minDurationMs: 20,
  includeClass: (className, moduleName) => moduleName !== 'BillingModule',
  includeMethod: (className, methodName) => !methodName.startsWith('_'),
});

Em qualquer framework

measure é o primitivo por trás disso e funciona sozinho:

const artists = await tracker.measure('resolveAvatars', () => Promise.all(rows.map(resolveAvatar)));

Diferença para tracker.span(): span cria o span sempre — é o certo para um trecho escolhido a dedo. measure guarda só dois números na chamada e descarta o que foi rápido, que é o que torna viável instrumentar tudo. Preserva função síncrona como síncrona.

O preço da troca: o span de measure é criado depois que o trecho terminou, então ele não é o pai dos spans criados lá dentro. Uma query feita dentro de um método medido aparece como irmã dele, não aninhada. Para aninhar, use tracker.span().

Atraso do event loop (automático)

Um trecho do request sem span nenhum tem duas causas possíveis e opostas: o processo parou (laço em memória, JSON gigante, cripto síncrona) ou o processo estava esperando alguém lá fora que ninguém instrumentou. As duas produzem exatamente o mesmo espaço em branco no waterfall.

O SDK mede o atraso do event loop durante cada requisição e, quando o loop realmente travou, grava no span raiz:

| atributo | significado | |---|---| | eventLoop.maxLagMs | pior travada isolada durante a requisição | | eventLoop.blockedMs | quanto do request o processo passou parado |

Abaixo do limiar (eventLoopLagThresholdMs, default 50 ms) nada é gravado — um atributo em todo span raiz só engordaria o payload. Então a presença do atributo já é o sinal.

A amostragem roda apenas enquanto há requisição aberta: processo ocioso não gasta nada, e o congelamento do Lambda entre invocações não vira falso positivo (um timer armado antes do congelamento dispararia segundos depois parecendo uma travada gigante).

createTracker({ apiKey, serviceName: 'minha-app', measureEventLoopLag: false }); // desliga

Spans manuais

await tracker.span('custom', 'render-pdf', async (span) => {
  span.setAttribute('pages', 12);
  // exceções são capturadas em span.error e relançadas
});

// ou controle total
const span = tracker.startSpan('custom', 'warmup-cache');
try { /* ... */ } finally { span.end(); }

tracker.currentSpan()?.setAttribute('userId', user.id);

Ingestão crua (qualquer linguagem)

Não precisa do SDK: basta enviar o body do contrato com a chave no header.

curl -X POST http://localhost:4050/v1/ingest \
  -H 'content-type: application/json' \
  -H 'x-fg-tracker-key: stk_demo_0000000000000000000000000000' \
  -d '{
    "traces": [{
      "traceId": "0123456789abcdef0123456789abcdef",
      "spans": [
        { "spanId": "0123456789abcdef", "parentSpanId": null, "kind": "http.server", "name": "GET /users/:id",
          "method": "GET", "route": "/users/:id", "statusCode": 200,
          "startedAt": "2026-09-12T12:00:00.000Z", "endedAt": "2026-09-12T12:00:00.123Z", "durationMs": 123.4,
          "attributes": { "url": "/users/42" }, "error": null },
        { "spanId": "fedcba9876543210", "parentSpanId": "0123456789abcdef", "kind": "db.query",
          "name": "select * from users where id = ?", "method": null, "route": null, "statusCode": null,
          "startedAt": "2026-09-12T12:00:00.010Z", "endedAt": "2026-09-12T12:00:00.050Z", "durationMs": 40,
          "attributes": { "rows": 1 }, "error": null }
      ]
    }]
  }'
# → 202 { "accepted": 2, "issuesDetected": 0 }

Logs (POST /v1/logs)

O SDK envia linhas de log para a plataforma em lote (flushIntervalMs 2 s, maxBatch 200, fila de 5000 descartando as mais antigas, 5xx/rede tenta de novo uma vez e devolve o lote ao próximo flush, 4xx descarta). Quando a linha nasce dentro de um span do tracker (request Express/Nest, tracker.span), traceId/spanId são anexados automaticamente e a linha aparece ligada ao trace no explorador /logs.

Shipper cru

import { createLogShipper } from 'fg-tracker-sdk';

const logs = createLogShipper({ apiKey: process.env.FG_TRACKER_KEY!, endpoint: 'http://localhost:4050', service: 'minha-api', env: 'production', tracker });
logs.push({ level: 'info', message: 'user logged in', context: { userId: 42 }, requestId: req.id });
logs.log('error', 'payment failed', { orderId });
await logs.flush(); await logs.shutdown();

| opção | default | descrição | |---|---|---| | apiKey / endpoint | — / http://localhost:4050 | iguais ao tracker | | service | — | nome do serviço (obrigatório) | | env, host | — / os.hostname() | ambiente e host; host: false desliga | | flushIntervalMs, maxBatch, maxQueueSize | 2000, 200, 5000 | lote e fila | | tracker | — | correlação com o span corrente (sem ele usa o contexto global do SDK, que funciona quando o tracker está no mesmo processo) | | minLevel | trace | nível mínimo enviado |

pino (pino-http, Fastify, NestJS com nestjs-pino)

Transport v7+ via pino-abstract-transport — roda em worker thread. Por isso o transport não enxerga o AsyncLocalStorage da aplicação: use traceMixin para carimbar traceId/spanId na thread principal.

import pino from 'pino';
import pinoHttp from 'pino-http';
import { pinoTransport, traceMixin } from 'fg-tracker-sdk';

const logger = pino({
  level: 'info',
  mixin: traceMixin(tracker),                       // opcional; correlação com traces
  transport: pinoTransport({ apiKey: process.env.FG_TRACKER_KEY!, endpoint: 'http://localhost:4050', service: 'minha-api', env: 'production' }),
  // equivalente: transport: { target: 'fg-tracker-sdk/pino', options: { apiKey, service } }
});
app.use(pinoHttp({ logger }));                      // req.id vira requestId
logger.info({ userId: 42 }, 'user logged in');

Mapeamento: level numérico → nível, msg → message, name → service (quando service não é passado), hostname → host, req.id/reqId/requestId → requestId, env → env; todas as outras chaves vão para context. Para enviar múltiplos destinos use pino.transport({ targets: [{ target: 'pino-pretty' }, pinoTransport({...})] }).

winston

import winston from 'winston';
import { FgTrackerTransport } from 'fg-tracker-sdk/winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
  transports: [
    new winston.transports.Console(),
    new FgTrackerTransport({ apiKey: process.env.FG_TRACKER_KEY!, service: 'minha-api', tracker }),
  ],
});
logger.warn('stock low', { sku: 'A-1' });      // meta → context

winston-transport é peer opcional (vem com o winston). Níveis npm/syslog fora do contrato são aproximados (crit/alert/emerg → fatal, verbose/silly → debug, http/notice → info). A instância expõe transport.shipper para flush()/shutdown().

NestJS Logger

Implementa a forma do LoggerService sem depender de @nestjs/common: espelha no console (desligue com console: false) e envia. O último parâmetro string vira context.context (nome da classe), e em error(msg, stack, context) o stack vai em context.stack.

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { createTracker, nestLogger } from 'fg-tracker-sdk';

export const tracker = createTracker({ apiKey: process.env.FG_TRACKER_KEY!, serviceName: 'minha-api' });
export const logger = nestLogger({ apiKey: process.env.FG_TRACKER_KEY!, service: 'minha-api', tracker });

const app = await NestFactory.create(AppModule, { logger });
app.use(tracker.express());
app.enableShutdownHooks();
process.on('SIGTERM', () => Promise.all([logger.shutdown(), tracker.shutdown()]).finally(() => process.exit(0)));

Dentro das classes continue usando new Logger(MinhaClasse.name) do Nest — ele delega ao logger da aplicação.

Ingestão crua de logs (qualquer linguagem)

curl -X POST http://localhost:4050/v1/logs \\
  -H 'content-type: application/json' \\
  -H 'x-fg-tracker-key: stk_demo_0000000000000000000000000000' \\
  -d '{ "logs": [
    { "ts": "2026-09-12T12:00:00.000Z", "level": "info", "service": "minha-api", "message": "user logged in",
      "context": { "userId": 42 }, "traceId": "0123456789abcdef0123456789abcdef", "requestId": "req-1", "env": "production" },
    { "level": "error", "service": "worker", "message": "ECONNRESET while calling payment gateway" }
  ] }'
# → 202 { "accepted": 2 }

Limites: 500 linhas por lote, 32 KB por mensagem, context truncado a 8 KB, ts fora da retenção vira now().

Infraestrutura

O tracker também envia agregados de infraestrutura por recurso (CONTRATO.md §27.4) que alimentam o diagrama vivo do painel. Regras:

  • Nunca amostrados: tracesSampleRate, tracesSampler e beforeSend valem só para spans; os contadores saem completos.
  • Agregação em processo: um resumo por recurso a cada 60 s (infra.idleIntervalMs), não um evento por chamada.
  • Mesmo lote do APM: os agregados vão em infra[] no mesmo POST /v1/ingest dos spans ({ traces: [], infra } quando só há infra).
  • Tempo real sob demanda: enquanto alguém observa o diagrama, a resposta da ingestão traz live: true e a janela cai para liveIntervalMs (5 s), sem chamada extra.
const tracker = createTracker({
  apiKey: process.env.FG_TRACKER_KEY!,
  endpoint: process.env.FG_TRACKER_URL,
  serviceName: 'orders-api',
  infra: { enabled: true, idleIntervalMs: 60_000 },   // defaults
});

AWS SDK v3 (SQS, SNS, EventBridge, Kinesis, DynamoDB, Step Functions)

import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

const sqs = tracker.instrumentAws(new SQSClient({}));
const dynamo = tracker.instrumentAws(new DynamoDBClient({}));   // instrumente o client base
const docs = DynamoDBDocumentClient.from(dynamo);               // o DocumentClient compartilha o middleware
await sqs.send(new SendMessageCommand({ QueueUrl, MessageBody }));

| serviço | operações | agregado | |---|---|---| | SQS | SendMessage[Batch], ReceiveMessage, DeleteMessage[Batch] | sqs: sent, bytesSent, received, receiveEmpty, deleted, errors (ARN resolvido da QueueUrl) | | SNS | Publish, PublishBatch | sns: published, bytes, errors | | EventBridge | PutEvents | eventbridge por barramento (EventBusName ou default): published, bytes, errors | | Kinesis | PutRecord, PutRecords | kinesis: published, bytes, errors | | DynamoDB | GetItem, PutItem, UpdateItem, DeleteItem, Query, Scan, BatchGetItem, BatchWriteItem, TransactGetItems, TransactWriteItems | dynamodb por tabela: readUnits, writeUnits, throttles, errors, calls | | Step Functions | StartExecution, StartSyncExecution | sfn: started, succeeded, failed |

  • No DynamoDB o middleware injeta ReturnConsumedCapacity: 'TOTAL' quando você não informa (passar 'NONE' desliga a contagem de unidades).
  • Quando há um trace em andamento, toda chamada AWS (de qualquer serviço) vira um span http.client AWS <serviço> <operação> <recurso> com attributes['aws.service'], aws.operation, aws.resourceArn, aws.region e statusCode/erro. Esses spans seguem a amostragem normal.
  • Nomes viram ARN quando a conta é conhecida (aprendida de ARNs/URLs vistos ou AWS_ACCOUNT_ID); sem conta, a API casa o nó por nome + região + serviço.

Consumidores de fila

for (const message of Messages ?? []) {
  await tracker.trackMessage(QueueUrl, () => handle(message));   // URL ou ARN da fila
}

Mede processingMsSum/processingMsMax e conta errors quando fn lança (o erro é relançado). Cria um span custom SQS process <fila> para os spans internos formarem um trace.

Pool de conexões (pg)

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
tracker.instrumentPg(pool);                             // spans db.query (inalterado)
tracker.instrumentPgPool(pool, { name: 'orders-db' });  // agregado db_pool

Lê totalCount, idleCount e waitingCount a cada 5 s (timer unref) e mede a espera de pool.connect — inclusive a usada por pool.query — em maxWaitMs. pool.end() para o timer.

ECS e Fargate

const reporter = tracker.startInfraReporter({ ecs: true, intervalMs: 10_000 });
// reporter.stop() — também parado por tracker.shutdown()

Com ECS_CONTAINER_METADATA_URI_V4 presente, lê /task uma vez (ARN da task, cluster, serviço — ECS_SERVICE_NAME, ServiceName ou a família da task definition —, launch type e limites) e /task/stats a cada intervalMs: ecs_task com cpuPercent (pico, relativo ao limite de CPU da task), memoryMb (sem cache), networkRxBytes/networkTxBytes (deltas), memoryLimitMb, cpuUnits, launchType, architecture e o heartbeat de cada janela, que conta as réplicas vivas. Fora do ECS é no-op.

AWS Lambda

export const handler = tracker.wrapLambda(async (event, context) => {
  // ...
  return { statusCode: 200 };
}, { flushTimeoutMs: 1000 });
  • Cria um span raiz custom lambda <função> por invocação (faas.coldStart, faas.requestId); desligue com { span: false }.
  • Sem a camada da extensão: aguarda flush() (até flushTimeoutMs) ao fim de cada invocação, para spans e agregados não se perderem no congelamento do ambiente.
  • Com a camada (/opt/extensions/fg-tracker-extension existe): não bloqueia a resposta; a fila sobrevive ao congelamento e é enviada no SIGTERM do encerramento (quando nenhum outro handler de SIGTERM está registrado).
  • Handlers com callback não são suportados (use async).

A camada fg-tracker-lambda-extension coleta, sem mudar o código, invocações, erros, timeouts, cold starts, duração, memória e concorrência (kind: 'lambda') pela Telemetry API. Anexe o ARN público da camada à função e defina FG_TRACKER_URL e FG_TRACKER_KEY.

Variáveis de ambiente

| variável | uso | |---|---| | FG_TRACKER_KEY | chave do projeto (CLIs; nos exemplos, apiKey) | | FG_TRACKER_URL | URL da API (CLIs; nos exemplos, endpoint) | | FG_TRACKER_RELEASE | release explícita (tracker e fg-tracker-sourcemaps) | | FG_TRACKER_ENV | ambiente (tracker e fg-tracker-release) | | FG_TRACKER_SERVICE | serviço padrão do fg-tracker-release |

O SDK lê o nome novo primeiro e, se estiver ausente ou vazio, o nome legado STREAD_TRACK_*. readEnv('KEY') expõe a mesma regra para o seu código.

Migração de stread-track-sdk

O pacote foi renomeado de stread-track-sdk (0.3.0) para fg-tracker-sdk (0.4.0). Nada quebra: os nomes antigos continuam funcionando e serão removidos na 1.0.

| antigo | novo | compatibilidade | |---|---|---| | pacote stread-track-sdk | fg-tracker-sdk | a 0.3.0 antiga segue no npm e continua enviando para a API | | bins stread-track-release, stread-track-sourcemaps | fg-tracker-release, fg-tracker-sourcemaps | os bins antigos são aliases obsoletos (mesmo arquivo, com aviso no stderr) | | STREAD_TRACK_KEY, STREAD_TRACK_URL, STREAD_TRACK_RELEASE, STREAD_TRACK_ENV, STREAD_TRACK_SERVICE | FG_TRACKER_KEY, FG_TRACKER_URL, FG_TRACKER_RELEASE, FG_TRACKER_ENV, FG_TRACKER_SERVICE | o novo tem precedência; o antigo é fallback | | header x-stread-track-key | x-fg-tracker-key | a API aceita os dois (e ?key=) | | StreadTrackTransport (/winston) | FgTrackerTransport | re-export @deprecated | | tipo StreadTrackNestLogger | FgTrackerNestLogger | alias @deprecated | | pino target: 'stread-track-sdk/pino' | target: 'fg-tracker-sdk/pino' | troque junto com o nome do pacote | | prefixo de log [stread-track] | [fg-tracker] | — |

Passo a passo: pnpm remove stread-track-sdk && pnpm add fg-tracker-sdk@^0.4.0, troque os imports (from 'stread-track-sdk' → from 'fg-tracker-sdk', inclusive /pino e /winston) e, quando quiser, renomeie as variáveis de ambiente e os bins no CI. A API precisa aceitar x-fg-tracker-key (versão da API a partir desta mudança) — a produção do FG Track já aceita.

Build

pnpm --filter fg-tracker-sdk build      # tsc → dist/ (ESM + .d.ts)
pnpm --filter fg-tracker-sdk typecheck

Database adapters (0.2.0)

Reuse the HTTP request tracker. instrumentPostgres(sql) wraps Postgres.js lazy queries, Drizzle's unsafe/values path, transactions, savepoints and reserved connections. Do not also instrument Drizzle for the same operation. It records normalized SQL and execution duration, including pool wait, with the request's trace ID and current parent span. Reusing a pending query does not execute or record it twice. Streaming/cursor helpers need an explicit completion wrapper.

instrumentDatabase(client, { system, methods, statement?, callbacks? }) supports methods returning synchronous results, Promises, or final error-first callbacks. Instrument acquired transaction connections separately. Only provide statement for SQL, never bind values, MongoDB documents, Redis keys or connection URLs. This adapter cannot infer arbitrary cursor/stream completion; wrap consumption:

await tracker.traceDatabase(
  { system: 'mongodb', operation: 'tracks.find' },
  () => collection.find(filter).toArray(),
);
const row = tracker.traceDatabaseSync(
  { system: 'sqlite', operation: 'tracks.get', statement: 'select * from tracks where id = ?' },
  () => statement.get(id),
);

These completion wrappers work with any engine, without installing its driver in the SDK. Unknown drivers require explicit binding at the operation boundary; there is no universal automatic monkey patch. Operations outside an active trace are executed normally without orphan query traces. Database error values remain in the application; exports contain a generic error and an optional error code. Use track_detect_databases in the MCP to select the adapter from the actual application dependencies/imports and then implement it at the active client.