From 0fa9f2b89f8b9ab0fb09457e21a94fcdc720a154 Mon Sep 17 00:00:00 2001 From: ibadus Date: Fri, 24 Apr 2026 13:10:13 +0200 Subject: [PATCH 1/2] feat: autumn-js pre-1.0 compat + local Jaeger trace playground Two loosely-related changes landing together: autumn-js pre-1.0 compatibility - Widen `autumn-js` peer range to `>=0.0.70 <2.0.0` (was `>=1.0.0`). - Wrap pre-1.0 flat-top-level methods (`attach`, `cancel`, `setupPayment`, `usage`) alongside the existing 1.x sub-resource coverage; methods missing from the installed SDK are skipped silently. - Unwrap the pre-1.0 `Result` response envelope so response-side span attributes populate the same on both versions; map `product_id`/`product_ids` to `autumn.plan_id`/`autumn.plan_ids` for dashboard consistency. - Expand unit tests to cover the pre-1.0 shapes. Local Jaeger trace playground (new) - `docker-compose.yml` at repo root runs `jaegertracing/all-in-one` with OTLP enabled on ports 16686 (UI), 4318 (OTLP HTTP), 4317 (OTLP gRPC). - Each package gets an `examples/` folder with `otel-setup.ts`, `demo.ts`, and a README. Demos use plain-object mocks (matching the unit tests) and export via OTLP/HTTP. - Autumn demo exercises every 1.x instrumented method (37 spans incl. one ERROR path). Drizzle demo exercises all instrumentation surfaces: `instrumentDrizzle` (query + execute + callback), `instrumentDrizzleClient` (session.prepareQuery, session.query, $client fallback, _.session.execute fallback, transactions), all SQL-verb operations, all query-object shapes, long-query truncation, and both promise/callback error paths (30 spans). - Root README gets a brief `## Examples` pointer; full walkthrough lives in a new `EXAMPLES.md`. - Dev-only: new `@opentelemetry/sdk-trace-node`, `exporter-trace-otlp-http`, `resources`, `semantic-conventions`, and `tsx` devDeps; `examples/` is outside `src/**/*` so it is neither type-checked into `dist/` nor published. Co-Authored-By: Claude Opus 4.7 (1M context) --- EXAMPLES.md | 67 +++ README.md | 6 + docker-compose.yml | 10 + packages/otel-autumn/README.md | 11 +- packages/otel-autumn/examples/README.md | 103 ++++ packages/otel-autumn/examples/demo.ts | 348 +++++++++++ packages/otel-autumn/examples/otel-setup.ts | 28 + packages/otel-autumn/package.json | 8 +- packages/otel-autumn/src/index.test.ts | 166 ++++++ packages/otel-autumn/src/index.ts | 149 +++-- packages/otel-drizzle/examples/README.md | 117 ++++ packages/otel-drizzle/examples/demo.ts | 263 +++++++++ packages/otel-drizzle/examples/otel-setup.ts | 28 + packages/otel-drizzle/package.json | 6 + pnpm-lock.yaml | 571 +++++++++++++++++++ 15 files changed, 1845 insertions(+), 36 deletions(-) create mode 100644 EXAMPLES.md create mode 100644 docker-compose.yml create mode 100644 packages/otel-autumn/examples/README.md create mode 100644 packages/otel-autumn/examples/demo.ts create mode 100644 packages/otel-autumn/examples/otel-setup.ts create mode 100644 packages/otel-drizzle/examples/README.md create mode 100644 packages/otel-drizzle/examples/demo.ts create mode 100644 packages/otel-drizzle/examples/otel-setup.ts diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..019056d --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,67 @@ +# Examples — visualize traces locally with Jaeger UI + +Each instrumentation package ships an `examples/` playground that replays a realistic workload through the instrumentation and exports the resulting spans to a local Jaeger via OTLP/HTTP. Use it to sanity-check that the traces look right in an actual observability UI — span tree, timings, kind, and attributes — without standing up a real Autumn/Drizzle environment. + +## Prerequisites + +- Docker (Jaeger runs in a container) +- pnpm + Node ≥ 18.19 / 20.6 (same as the packages themselves) + +## 1. Start Jaeger + +From the repo root: + +```bash +docker compose up -d +``` + +This launches `jaegertracing/all-in-one` with OTLP enabled, exposing: + +- `http://localhost:16686` — Jaeger UI +- `http://localhost:4318` — OTLP HTTP endpoint (what the demos push to) +- `http://localhost:4317` — OTLP gRPC endpoint + +## 2. Install workspace dependencies (first time only) + +```bash +pnpm install +``` + +## 3. Run a demo + +Pick the package you want to exercise. Each demo uses **plain-object mocks** of the underlying SDK — no API keys, no running Postgres. + +```bash +# Autumn — replays all 36 instrumented methods + 1 error path (37 spans total) +pnpm --filter @api-blitz/otel-autumn example + +# Drizzle — replays every instrumentation surface (30 spans: all SQL verbs, +# query-object shapes, transactions, callback pattern, error path, truncation) +pnpm --filter @api-blitz/otel-drizzle example +``` + +You can run both back-to-back; each writes to a different service name so the traces don't collide. + +## 4. Inspect traces + +Open , then in the **Service** dropdown pick: + +- `otel-autumn-demo` — every `autumn.*` operation +- `otel-drizzle-demo` — every `drizzle.*` operation + +Click **Find Traces** and drill into any trace to see its attributes. Each per-package `examples/README.md` has a full table of expected span names and attributes so you can cross-reference what you're seeing: + +- [otel-autumn example](./packages/otel-autumn/examples/README.md) +- [otel-drizzle example](./packages/otel-drizzle/examples/README.md) + +## 5. Stop Jaeger + +```bash +docker compose down +``` + +## Troubleshooting + +- **`ECONNREFUSED` from the demo** — Jaeger isn't up, or `:4318` is being used by something else. Run `docker compose up -d` first. +- **Pointing at a different OTLP collector** — set `OTEL_EXPORTER_OTLP_ENDPOINT`, e.g. `OTEL_EXPORTER_OTLP_ENDPOINT=http://my-collector:4318 pnpm --filter @api-blitz/otel-autumn example`. +- **Traces stick around between runs** — Jaeger's in-memory storage retains traces until the container restarts. `docker compose restart` clears them. diff --git a/README.md b/README.md index a0c40ee..d26b1f9 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,12 @@ Versions follow semver. Pin with `^1.0.0` (caret) to get patch and minor updates --- +## Examples + +Want to see what the traces actually look like before you install? Each package ships a playground that replays a realistic workload through the instrumentation and exports the resulting spans to a local Jaeger UI — no API keys, no database required. See **[EXAMPLES.md](./EXAMPLES.md)**. + +--- + ## Releasing (maintainers) Releases go out by hand from a local checkout. Per change: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d2f6aef --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + jaeger: + image: jaegertracing/all-in-one:1.62.0 + container_name: otel-davis-jaeger + environment: + - COLLECTOR_OTLP_ENABLED=true + ports: + - "16686:16686" + - "4317:4317" + - "4318:4318" diff --git a/packages/otel-autumn/README.md b/packages/otel-autumn/README.md index b197e20..76b8073 100644 --- a/packages/otel-autumn/README.md +++ b/packages/otel-autumn/README.md @@ -15,7 +15,7 @@ yarn add @api-blitz/otel-autumn bun add @api-blitz/otel-autumn ``` -**Peer dependencies:** `@opentelemetry/api` >= 1.9.0, `autumn-js` >= 1.0.0 < 2.0.0 +**Peer dependencies:** `@opentelemetry/api` >= 1.9.0, `autumn-js` >= 0.0.70 < 2.0.0 ## Quick start @@ -37,6 +37,15 @@ await autumn.billing.attach({ customerId: "cus_123", planId: "pro" }); `instrumentAutumn` wraps the Autumn client instance you already use — no configuration changes needed. Every SDK call creates a `CLIENT` span with operation-specific attributes, and the same client instance is returned so instrumentation is idempotent (calling it twice is a no-op). +## Version compatibility + +Works across `autumn-js` from the last pre-1.0 releases (`>= 0.0.70`) through the current 1.x line. + +- **1.x** — full 36-method coverage across `check`, `track`, and every `billing.*` / `customers.*` / `entities.*` / `balances.*` / `events.*` / `plans.*` / `features.*` / `referrals.*` sub-resource. +- **Pre-1.0 (0.0.70 – 0.0.80)** — `check` and `track`, plus the flat top-level billing methods that existed before the 1.x rename: `attach`, `cancel`, `setupPayment`, `usage`. Pre-1.0's `Result` response envelope is unwrapped automatically so response-side span attributes are populated the same way as on 1.x. Pre-1.0's `product_id` / `product_ids` are surfaced under the `autumn.plan_id` / `autumn.plan_ids` attribute so dashboards stay consistent across versions. + +Methods that don't exist on the installed SDK version are skipped silently — instrumenting a pre-1.0 client doesn't fail because `autumn.billing` / `autumn.plans` aren't present. + ## What gets traced The instrumentation wraps every method on the Autumn SDK client — 2 top-level entry points plus 34 sub-resource operations across 8 namespaces. diff --git a/packages/otel-autumn/examples/README.md b/packages/otel-autumn/examples/README.md new file mode 100644 index 0000000..d6839b1 --- /dev/null +++ b/packages/otel-autumn/examples/README.md @@ -0,0 +1,103 @@ +# otel-autumn trace playground + +Replays **every** instrumented Autumn method through `instrumentAutumn` and exports the spans via OTLP/HTTP to a local Jaeger. Use this to visually verify that the instrumentation covers the full SDK surface with the expected span names and attributes. + +## Run it + +From the repo root: + +```bash +docker compose up -d # starts Jaeger on :16686 and :4318 +pnpm --filter @api-blitz/otel-autumn install # first time only +pnpm --filter @api-blitz/otel-autumn example +``` + +Then open , pick **Service** `otel-autumn-demo`, and click **Find Traces**. + +When finished: + +```bash +docker compose down +``` + +## What you should see + +**37 spans total**: 36 successful `CLIENT`-kind spans covering every method on the 1.x client surface, plus one `autumn.check` span with `otel.status_code=ERROR`. + +The demo runs with `captureCustomerData: true` so `autumn.payment_url` and `autumn.portal_url` are populated (they're redacted by default — the unit tests cover the redacted case). + +### Top-level (2 spans) + +| Span | Key attributes | +|---|---| +| `autumn.check` | `autumn.allowed=true`, `autumn.balance=42`, `autumn.feature_id=messages`, `autumn.plan_id=pro`, `autumn.flag_id=flag_demo`, `autumn.has_preview=true` | +| `autumn.track` | `autumn.event_name=message_sent`, `autumn.value=1`, `autumn.balance=41`, `autumn.balance_count=2` | + +### billing (8 spans) + +| Span | Key attributes | +|---|---| +| `autumn.billing.attach` | `autumn.plan_id=pro`, `autumn.plan_version=2`, `autumn.invoice_mode=true`, `autumn.feature_quantities_count=2`, `autumn.discount_count=1`, `autumn.has_payment_url=true`, `autumn.invoice_id=in_demo`, `autumn.currency=usd`, `autumn.total_amount=2000` | +| `autumn.billing.multiAttach` | `autumn.plan_ids=pro,addon_seats`, `autumn.plan_count=2` | +| `autumn.billing.previewAttach` | `autumn.total_amount=2000`, `autumn.has_prorations=true` | +| `autumn.billing.previewMultiAttach` | `autumn.total_amount=5000`, `autumn.has_prorations=false` | +| `autumn.billing.update` | `autumn.cancel_action=cancel_end_of_cycle`, `autumn.proration_behavior=none`, `autumn.plan_version=3` | +| `autumn.billing.previewUpdate` | `autumn.total_amount=1000`, `autumn.has_prorations=true` | +| `autumn.billing.openCustomerPortal` | `autumn.has_portal_url=true`, `autumn.portal_url=https://billing.stripe.com/...` | +| `autumn.billing.setupPayment` | `autumn.has_payment_url=true`, `autumn.payment_url=https://checkout.stripe.com/setup/...` | + +### customers (4 spans) + +`autumn.customers.getOrCreate`, `.list`, `.update`, `.delete` — each carries `autumn.customer_id=cus_demo`. + +### entities (4 spans) + +`autumn.entities.create`, `.get`, `.update`, `.delete` — each carries `autumn.entity_id=seat_demo` and `autumn.entity_feature_id=seats`. + +### balances (4 spans) + +`autumn.balances.create`, `.update`, `.delete`, `.finalize` — each carries `autumn.feature_id=messages` and `autumn.balance` (the remaining value from the response). + +### events (2 spans) + +| Span | Key attributes | +|---|---| +| `autumn.events.list` | `autumn.event_count=3`, `autumn.has_more=false` | +| `autumn.events.aggregate` | `autumn.aggregate_range=7d`, `autumn.feature_count=2`, `autumn.period_count=2`, `autumn.event_count=7` (sum), `autumn.value=1578` (sum) | + +### plans (5 spans) + +`autumn.plans.create`, `.get`, `.list`, `.update`, `.delete` — each non-`list` span carries `autumn.plan_id=pro` and `autumn.plan_name`. + +### features (5 spans) + +`autumn.features.create`, `.get`, `.list`, `.update`, `.delete` — each non-`list` span carries `autumn.feature_id=messages`, `autumn.feature_name`, `autumn.feature_type=metered`. + +### referrals (2 spans) + +`autumn.referrals.createCode`, `autumn.referrals.redeemCode` — each carries `autumn.referral_code=REF123` and `autumn.referral_program_id=prog_demo`. + +### Error path (1 span) + +A second `autumn.check` span with `otel.status_code=ERROR`, `error=true`, and an `exception` event with message `demo: feature not found`. + +## Quick Jaeger API checks + +After running the demo, these commands validate the trace set programmatically: + +```bash +# 36 unique operation names expected +curl -s 'http://localhost:16686/api/services/otel-autumn-demo/operations' \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['total'])" + +# 37 spans total (36 successes + 1 error) +curl -s 'http://localhost:16686/api/traces?service=otel-autumn-demo&limit=200' \ + | python3 -c "import sys,json; d=json.load(sys.stdin); print(sum(len(t['spans']) for t in d['data']))" +``` + +## Notes + +- The demo uses a **plain-object mock** of the `autumn-js` client (same pattern as the unit tests), so no Autumn API key or network access is required. +- Pre-1.0 `autumn-js` (0.0.x) top-level methods (`attach`, `cancel`, `setupPayment`, `usage`) are instrumented but only wrap if the method exists on the client — they're absent from a 1.x shape, so the demo doesn't exercise them. The unit tests cover that compatibility surface. +- Spans are pushed via `BatchSpanProcessor`; the demo explicitly calls `forceFlush()` and `shutdown()` before exit so nothing is dropped. +- OTLP endpoint defaults to `http://localhost:4318`; override with `OTEL_EXPORTER_OTLP_ENDPOINT` if Jaeger runs elsewhere. diff --git a/packages/otel-autumn/examples/demo.ts b/packages/otel-autumn/examples/demo.ts new file mode 100644 index 0000000..bff5e99 --- /dev/null +++ b/packages/otel-autumn/examples/demo.ts @@ -0,0 +1,348 @@ +import { instrumentAutumn } from "../src/index"; +import { setupOtel } from "./otel-setup"; + +function createMockAutumnClient() { + return { + check: async (_args: unknown) => ({ + allowed: true, + customerId: "cus_demo", + featureId: "messages", + balance: { + featureId: "messages", + remaining: 42, + granted: 100, + usage: 58, + unlimited: false, + overageAllowed: false, + maxPurchase: null, + nextResetAt: null, + feature: { name: "Messages", type: "metered" }, + }, + flag: { id: "flag_demo", planId: "pro", featureId: "messages", expiresAt: null }, + preview: { scenario: "upgrade" }, + }), + track: async (_args: unknown) => ({ + customerId: "cus_demo", + featureId: "messages", + eventName: "message_sent", + value: 1, + balance: { + featureId: "messages", + remaining: 41, + granted: 100, + usage: 59, + }, + balances: { messages: { remaining: 41 }, api_calls: { remaining: 900 } }, + }), + billing: { + attach: async (_args: unknown) => ({ + customerId: "cus_demo", + paymentUrl: "https://checkout.stripe.com/pay/cs_demo", + invoice: { + stripeId: "in_demo", + status: "paid", + total: 2000, + currency: "usd", + hostedInvoiceUrl: "https://invoice.stripe.com/demo", + }, + requiredAction: { code: "none" }, + }), + multiAttach: async (_args: unknown) => ({ + customerId: "cus_demo", + paymentUrl: null, + }), + previewAttach: async (_args: unknown) => ({ + customerId: "cus_demo", + total: 2000, + currency: "usd", + hasProrations: true, + }), + previewMultiAttach: async (_args: unknown) => ({ + customerId: "cus_demo", + total: 5000, + currency: "usd", + hasProrations: false, + }), + update: async (_args: unknown) => ({ + customerId: "cus_demo", + paymentUrl: null, + invoice: { stripeId: "in_demo_2", status: "draft", total: 500, currency: "usd" }, + }), + previewUpdate: async (_args: unknown) => ({ + customerId: "cus_demo", + total: 1000, + currency: "usd", + hasProrations: true, + }), + openCustomerPortal: async (_args: unknown) => ({ + customerId: "cus_demo", + url: "https://billing.stripe.com/session/demo", + }), + setupPayment: async (_args: unknown) => ({ + customerId: "cus_demo", + url: "https://checkout.stripe.com/setup/cs_demo", + }), + }, + customers: { + getOrCreate: async (_args: unknown) => ({ id: "cus_demo", name: "Ada", email: "ada@example.com" }), + list: async () => ({ items: [{ id: "cus_demo" }, { id: "cus_demo_2" }] }), + update: async (_args: unknown) => ({ id: "cus_demo" }), + delete: async (_args: unknown) => ({ id: "cus_demo" }), + }, + entities: { + create: async (_args: unknown) => ({ id: "seat_demo", customerId: "cus_demo", featureId: "seats" }), + get: async (_args: unknown) => ({ id: "seat_demo", customerId: "cus_demo", featureId: "seats" }), + update: async (_args: unknown) => ({ id: "seat_demo", customerId: "cus_demo", featureId: "seats" }), + delete: async (_args: unknown) => ({ id: "seat_demo", customerId: "cus_demo", featureId: "seats" }), + }, + balances: { + create: async (_args: unknown) => ({ featureId: "messages", remaining: 10 }), + update: async (_args: unknown) => ({ featureId: "messages", remaining: 20 }), + delete: async (_args: unknown) => ({ featureId: "messages", remaining: 0 }), + finalize: async (_args: unknown) => ({ featureId: "messages", remaining: 15 }), + }, + events: { + list: async (_args: unknown) => ({ + list: [{ id: "evt_1" }, { id: "evt_2" }, { id: "evt_3" }], + hasMore: false, + offset: 0, + limit: 50, + total: 3, + }), + aggregate: async (_args: unknown) => ({ + list: [ + { period: 1700000000000, values: { messages: 512 } }, + { period: 1700086400000, values: { messages: 1024 } }, + ], + total: { messages: { count: 2, sum: 1536 }, api_calls: { count: 5, sum: 42 } }, + }), + }, + plans: { + create: async (_args: unknown) => ({ planId: "pro", id: "pro", name: "Pro" }), + get: async (_args: unknown) => ({ planId: "pro", id: "pro", name: "Pro" }), + list: async () => ({ items: [{ id: "pro" }, { id: "starter" }] }), + update: async (_args: unknown) => ({ planId: "pro", id: "pro", name: "Pro (Updated)" }), + delete: async (_args: unknown) => ({ planId: "pro", id: "pro" }), + }, + features: { + create: async (_args: unknown) => ({ featureId: "messages", id: "messages", name: "Messages", type: "metered" }), + get: async (_args: unknown) => ({ featureId: "messages", id: "messages", name: "Messages", type: "metered" }), + list: async () => ({ items: [{ id: "messages" }, { id: "api_calls" }] }), + update: async (_args: unknown) => ({ featureId: "messages", id: "messages", name: "Messages v2", type: "metered" }), + delete: async (_args: unknown) => ({ featureId: "messages", id: "messages" }), + }, + referrals: { + createCode: async (_args: unknown) => ({ code: "REF123", programId: "prog_demo" }), + redeemCode: async (_args: unknown) => ({ code: "REF123", programId: "prog_demo" }), + }, + }; +} + +async function main() { + const provider = setupOtel("otel-autumn-demo"); + + const client = createMockAutumnClient(); + instrumentAutumn(client as never, { captureCustomerData: true }); + + // --- Top-level (2) --- + await client.check({ + customerId: "cus_demo", + featureId: "messages", + requiredBalance: 3, + sendEvent: true, + withPreview: true, + } as never); + + await client.track({ + customerId: "cus_demo", + featureId: "messages", + eventName: "message_sent", + value: 1, + } as never); + + // --- billing (8) --- + await client.billing.attach({ + customerId: "cus_demo", + planId: "pro", + version: 2, + invoiceMode: { enabled: true }, + prorationBehavior: "create_prorations", + redirectMode: "return_url", + carryOverBalances: { enabled: true, featureIds: ["messages"] }, + carryOverUsages: { enabled: false }, + noBillingChanges: false, + newBillingSubscription: true, + featureQuantities: [ + { featureId: "seats", quantity: 5 }, + { featureId: "api_calls", quantity: 10_000 }, + ], + discounts: [{ code: "SAVE10" }], + } as never); + + await client.billing.multiAttach({ + customerId: "cus_demo", + plans: [{ planId: "pro" }, { planId: "addon_seats" }], + invoiceMode: { enabled: false }, + redirectMode: "return_url", + newBillingSubscription: false, + discounts: [], + } as never); + + await client.billing.previewAttach({ + customerId: "cus_demo", + planId: "pro", + } as never); + + await client.billing.previewMultiAttach({ + customerId: "cus_demo", + plans: [{ planId: "pro" }], + } as never); + + await client.billing.update({ + customerId: "cus_demo", + planId: "pro", + cancelAction: "cancel_end_of_cycle", + prorationBehavior: "none", + version: 3, + featureQuantities: [{ featureId: "seats", quantity: 10 }], + } as never); + + await client.billing.previewUpdate({ + customerId: "cus_demo", + planId: "pro", + } as never); + + await client.billing.openCustomerPortal({ customerId: "cus_demo" } as never); + + await client.billing.setupPayment({ customerId: "cus_demo" } as never); + + // --- customers (4) --- + await client.customers.getOrCreate({ + id: "cus_demo", + email: "ada@example.com", + } as never); + await client.customers.list(); + await client.customers.update({ id: "cus_demo" } as never); + await client.customers.delete({ id: "cus_demo" } as never); + + // --- entities (4) --- + await client.entities.create({ + customerId: "cus_demo", + entityId: "seat_demo", + featureId: "seats", + name: "Seat 1", + } as never); + await client.entities.get({ entityId: "seat_demo" } as never); + await client.entities.update({ entityId: "seat_demo" } as never); + await client.entities.delete({ entityId: "seat_demo" } as never); + + // --- balances (4) --- + await client.balances.create({ + customerId: "cus_demo", + featureId: "messages", + value: 10, + } as never); + await client.balances.update({ + customerId: "cus_demo", + featureId: "messages", + value: 20, + } as never); + await client.balances.delete({ + customerId: "cus_demo", + featureId: "messages", + } as never); + await client.balances.finalize({ + customerId: "cus_demo", + featureId: "messages", + lockId: "lock_demo", + } as never); + + // --- events (2) --- + await client.events.list({ + customerId: "cus_demo", + featureId: "messages", + } as never); + await client.events.aggregate({ + customerId: "cus_demo", + range: "7d", + } as never); + + // --- plans (5) --- + await client.plans.create({ planId: "pro", name: "Pro" } as never); + await client.plans.get({ planId: "pro" } as never); + await client.plans.list(); + await client.plans.update({ planId: "pro", name: "Pro v2" } as never); + await client.plans.delete({ planId: "pro" } as never); + + // --- features (5) --- + await client.features.create({ + featureId: "messages", + name: "Messages", + type: "metered", + } as never); + await client.features.get({ featureId: "messages" } as never); + await client.features.list(); + await client.features.update({ featureId: "messages", name: "Messages v2" } as never); + await client.features.delete({ featureId: "messages" } as never); + + // --- referrals (2) --- + await client.referrals.createCode({ + customerId: "cus_demo", + programId: "prog_demo", + } as never); + await client.referrals.redeemCode({ + customerId: "cus_demo", + code: "REF123", + } as never); + + // --- Error path (1) --- + const failing = { + check: async () => { + throw new Error("demo: feature not found"); + }, + }; + instrumentAutumn(failing as never); + try { + await failing.check(); + } catch { + // expected — shows up as ERROR span in Jaeger + } + + try { + await provider.forceFlush(); + await provider.shutdown(); + } catch (err) { + if (isConnRefused(err)) { + console.error( + "Could not reach the OTLP endpoint at http://localhost:4318. Is Jaeger running? Try `docker compose up -d` from the repo root.", + ); + process.exit(1); + } + throw err; + } + + console.log( + "Demo complete. 37 spans exported. Open http://localhost:16686 and pick service 'otel-autumn-demo'.", + ); +} + +function isConnRefused(err: unknown): boolean { + if (!err || typeof err !== "object") return false; + if (Array.isArray(err)) return err.some(isConnRefused); + const e = err as { code?: string; errors?: unknown[]; cause?: unknown }; + if (e.code === "ECONNREFUSED") return true; + if (Array.isArray(e.errors) && e.errors.some(isConnRefused)) return true; + if (e.cause && isConnRefused(e.cause)) return true; + return false; +} + +main().catch((err) => { + if (isConnRefused(err)) { + console.error( + "Could not reach the OTLP endpoint at http://localhost:4318. Is Jaeger running? Try `docker compose up -d` from the repo root.", + ); + process.exit(1); + } + console.error(err); + process.exit(1); +}); diff --git a/packages/otel-autumn/examples/otel-setup.ts b/packages/otel-autumn/examples/otel-setup.ts new file mode 100644 index 0000000..c8a3bdc --- /dev/null +++ b/packages/otel-autumn/examples/otel-setup.ts @@ -0,0 +1,28 @@ +import { trace } from "@opentelemetry/api"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { resourceFromAttributes } from "@opentelemetry/resources"; +import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; +import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; + +const JAEGER_OTLP_URL = + process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.replace(/\/$/, "") ?? + "http://localhost:4318"; + +export function setupOtel(serviceName: string): NodeTracerProvider { + const provider = new NodeTracerProvider({ + resource: resourceFromAttributes({ + [ATTR_SERVICE_NAME]: serviceName, + }), + spanProcessors: [ + new BatchSpanProcessor( + new OTLPTraceExporter({ + url: `${JAEGER_OTLP_URL}/v1/traces`, + timeoutMillis: 3000, + }), + ), + ], + }); + trace.setGlobalTracerProvider(provider); + return provider; +} diff --git a/packages/otel-autumn/package.json b/packages/otel-autumn/package.json index 1528609..2a6c05a 100644 --- a/packages/otel-autumn/package.json +++ b/packages/otel-autumn/package.json @@ -31,6 +31,7 @@ "scripts": { "build": "pnpm clean && tsc", "clean": "rimraf dist", + "example": "tsx examples/demo.ts", "prepublishOnly": "pnpm build", "type-check": "tsc --noEmit", "unit-test": "vitest --run", @@ -39,15 +40,20 @@ "dependencies": {}, "devDependencies": { "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", + "@opentelemetry/resources": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.1.0", + "@opentelemetry/sdk-trace-node": "^2.1.0", + "@opentelemetry/semantic-conventions": "^1.37.0", "@types/node": "18.15.11", "autumn-js": "^1.2.11", "rimraf": "3.0.2", + "tsx": "^4.19.2", "typescript": "^5", "vitest": "0.33.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", - "autumn-js": ">=1.0.0 <2.0.0" + "autumn-js": ">=0.0.70 <2.0.0" } } diff --git a/packages/otel-autumn/src/index.test.ts b/packages/otel-autumn/src/index.test.ts index 9926110..065cdce 100644 --- a/packages/otel-autumn/src/index.test.ts +++ b/packages/otel-autumn/src/index.test.ts @@ -546,4 +546,170 @@ describe("@api-blitz/otel-autumn", () => { expect(spans.filter((s) => s.name === "autumn.track").length).toBe(1); }); }); + + describe("pre-1.0 compatibility", () => { + function createMockPreV1Client() { + // Mirrors autumn-js 0.0.80: top-level attach/cancel/setupPayment/usage + // with snake_case fields, responses wrapped as Result = { data, error }. + return { + check: vi.fn().mockResolvedValue({ + data: { + allowed: true, + customer_id: "cus_1", + balance: { feature_id: "messages", remaining: 42 }, + }, + error: null, + }), + track: vi.fn().mockResolvedValue({ + data: { + id: "evt_1", + customer_id: "cus_1", + feature_id: "messages", + }, + error: null, + }), + attach: vi.fn().mockResolvedValue({ + data: { + checkout_url: "https://checkout.stripe.com/pay/cs_test", + customer_id: "cus_1", + product_ids: ["pro"], + code: "ok", + message: "", + }, + error: null, + }), + cancel: vi.fn().mockResolvedValue({ + data: { success: true, customer_id: "cus_1", product_id: "pro" }, + error: null, + }), + setupPayment: vi.fn().mockResolvedValue({ + data: { customer_id: "cus_1", url: "https://checkout.stripe.com/setup/cs_test" }, + error: null, + }), + usage: vi.fn().mockResolvedValue({ + data: { code: "ok", customer_id: "cus_1", feature_id: "messages" }, + error: null, + }), + }; + } + + it("wraps top-level attach with product_id → plan_id and checkout_url", async () => { + const client = createMockPreV1Client(); + instrumentAutumn(client as never); + + await client.attach({ + customer_id: "cus_1", + product_id: "pro", + } as never); + + const span = findSpan("autumn.attach"); + expect(span.attributes[SEMATTRS_BILLING_OPERATION]).toBe("attach"); + expect(span.attributes[SEMATTRS_AUTUMN_RESOURCE]).toBe("attach"); + expect(span.attributes[SEMATTRS_AUTUMN_CUSTOMER_ID]).toBe("cus_1"); + expect(span.attributes[SEMATTRS_AUTUMN_PLAN_ID]).toBe("pro"); + expect(span.attributes[SEMATTRS_AUTUMN_HAS_PAYMENT_URL]).toBe(true); + expect(span.attributes[SEMATTRS_AUTUMN_PAYMENT_URL]).toBeUndefined(); + // product_ids array in response surfaces as plan_ids + expect(span.attributes[SEMATTRS_AUTUMN_PLAN_IDS]).toBe("pro"); + expect(span.attributes[SEMATTRS_AUTUMN_PLAN_COUNT]).toBe(1); + }); + + it("emits checkout_url when captureCustomerData is enabled", async () => { + const client = createMockPreV1Client(); + instrumentAutumn(client as never, { captureCustomerData: true }); + + await client.attach({ customer_id: "cus_1", product_id: "pro" } as never); + + const span = findSpan("autumn.attach"); + expect(span.attributes[SEMATTRS_AUTUMN_PAYMENT_URL]).toBe("https://checkout.stripe.com/pay/cs_test"); + }); + + it("wraps top-level cancel and maps cancel_immediately to cancel_action", async () => { + const client = createMockPreV1Client(); + instrumentAutumn(client as never); + + await client.cancel({ + customer_id: "cus_1", + product_id: "pro", + cancel_immediately: true, + } as never); + + const span = findSpan("autumn.cancel"); + expect(span.attributes[SEMATTRS_AUTUMN_CUSTOMER_ID]).toBe("cus_1"); + expect(span.attributes[SEMATTRS_AUTUMN_PLAN_ID]).toBe("pro"); + expect(span.attributes[SEMATTRS_AUTUMN_CANCEL_ACTION]).toBe("cancel_immediately"); + }); + + it("wraps top-level cancel with cancel_immediately false as end_of_cycle", async () => { + const client = createMockPreV1Client(); + instrumentAutumn(client as never); + + await client.cancel({ + customer_id: "cus_1", + product_id: "pro", + cancel_immediately: false, + } as never); + + const span = findSpan("autumn.cancel"); + expect(span.attributes[SEMATTRS_AUTUMN_CANCEL_ACTION]).toBe("cancel_end_of_cycle"); + }); + + it("wraps top-level setupPayment", async () => { + const client = createMockPreV1Client(); + instrumentAutumn(client as never); + + await client.setupPayment({ customer_id: "cus_1" } as never); + + const span = findSpan("autumn.setupPayment"); + expect(span.attributes[SEMATTRS_AUTUMN_CUSTOMER_ID]).toBe("cus_1"); + expect(span.attributes[SEMATTRS_AUTUMN_HAS_PAYMENT_URL]).toBe(true); + }); + + it("wraps top-level usage", async () => { + const client = createMockPreV1Client(); + instrumentAutumn(client as never); + + await client.usage({ + customer_id: "cus_1", + feature_id: "messages", + value: 5, + } as never); + + const span = findSpan("autumn.usage"); + expect(span.attributes[SEMATTRS_AUTUMN_CUSTOMER_ID]).toBe("cus_1"); + expect(span.attributes[SEMATTRS_AUTUMN_FEATURE_ID]).toBe("messages"); + expect(span.attributes[SEMATTRS_AUTUMN_VALUE]).toBe(5); + }); + + it("unwraps Result for check response annotation", async () => { + const client = createMockPreV1Client(); + instrumentAutumn(client as never); + + await client.check({ customer_id: "cus_1", feature_id: "messages" } as never); + + const span = findSpan("autumn.check"); + // request side reads snake_case + expect(span.attributes[SEMATTRS_AUTUMN_CUSTOMER_ID]).toBe("cus_1"); + expect(span.attributes[SEMATTRS_AUTUMN_FEATURE_ID]).toBe("messages"); + // response side reads through the Result wrapper + expect(span.attributes[SEMATTRS_AUTUMN_ALLOWED]).toBe(true); + expect(span.attributes[SEMATTRS_AUTUMN_BALANCE]).toBe(42); + }); + + it("does not crash when a pre-1.0 client lacks 1.x sub-resources", () => { + const client = createMockPreV1Client(); + // Note: no billing, customers, entities, balances, events, plans, features, referrals + expect(() => instrumentAutumn(client as never)).not.toThrow(); + }); + + it("remains idempotent on a pre-1.0-shaped client", async () => { + const client = createMockPreV1Client(); + instrumentAutumn(client as never); + instrumentAutumn(client as never); + + await client.attach({ customer_id: "cus_1", product_id: "pro" } as never); + + expect(exporter.getFinishedSpans().length).toBe(1); + }); + }); }); diff --git a/packages/otel-autumn/src/index.ts b/packages/otel-autumn/src/index.ts index 1a00565..d535b06 100644 --- a/packages/otel-autumn/src/index.ts +++ b/packages/otel-autumn/src/index.ts @@ -176,6 +176,18 @@ function readEnabled(value: unknown): boolean | undefined { return undefined; } +// Pre-1.0 autumn-js wraps responses as Result = { data, error }. The +// 1.x SDK returns bare payloads. Unwrap only the success branch so response +// annotators see the same shape on both versions; 1.x values pass through +// unchanged because they don't carry a `data`/`error` pair. +function unwrapResult(value: unknown): unknown { + if (!isObject(value)) return value; + if ("data" in value && "error" in value && value.data != null) { + return value.data; + } + return value; +} + function extractPlanIds(plans: unknown): string[] | undefined { if (!Array.isArray(plans)) return undefined; const ids = plans @@ -188,12 +200,13 @@ function extractPlanIds(plans: unknown): string[] | undefined { const annotateCheckRequest: Annotator = (span, req) => { if (!isObject(req)) return; - setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, req.customerId); - setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, req.featureId); - setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, req.entityId); - setIfNumber(span, SEMATTRS_AUTUMN_REQUIRED_BALANCE, req.requiredBalance); - setIfBoolean(span, SEMATTRS_AUTUMN_SEND_EVENT, req.sendEvent); - setIfBoolean(span, SEMATTRS_AUTUMN_WITH_PREVIEW, req.withPreview); + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, req.customerId ?? req.customer_id); + setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, req.featureId ?? req.feature_id); + setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, req.entityId ?? req.entity_id); + setIfString(span, SEMATTRS_AUTUMN_PLAN_ID, req.planId ?? req.product_id); + setIfNumber(span, SEMATTRS_AUTUMN_REQUIRED_BALANCE, req.requiredBalance ?? req.required_balance); + setIfBoolean(span, SEMATTRS_AUTUMN_SEND_EVENT, req.sendEvent ?? req.send_event); + setIfBoolean(span, SEMATTRS_AUTUMN_WITH_PREVIEW, req.withPreview ?? req.with_preview); if (isObject(req.lock)) { setIfString(span, SEMATTRS_AUTUMN_LOCK, req.lock.lockId); } @@ -202,12 +215,14 @@ const annotateCheckRequest: Annotator = (span, req) => { const annotateCheckResponse: Annotator = (span, res) => { if (!isObject(res)) return; setIfBoolean(span, SEMATTRS_AUTUMN_ALLOWED, res.allowed); - setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId); - setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, res.entityId); - setIfNumber(span, SEMATTRS_AUTUMN_REQUIRED_BALANCE, res.requiredBalance); + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId ?? res.customer_id); + setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, res.entityId ?? res.entity_id); + setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, res.featureId ?? res.feature_id); + setIfString(span, SEMATTRS_AUTUMN_PLAN_ID, res.planId ?? res.product_id); + setIfNumber(span, SEMATTRS_AUTUMN_REQUIRED_BALANCE, res.requiredBalance ?? res.required_balance); if (isObject(res.balance)) { setIfNumber(span, SEMATTRS_AUTUMN_BALANCE, res.balance.remaining); - setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, res.balance.featureId); + setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, res.balance.featureId ?? res.balance.feature_id); if (isObject(res.balance.feature)) { setIfString(span, SEMATTRS_AUTUMN_FEATURE_NAME, res.balance.feature.name); setIfString(span, SEMATTRS_AUTUMN_FEATURE_TYPE, res.balance.feature.type); @@ -215,7 +230,7 @@ const annotateCheckResponse: Annotator = (span, res) => { } if (isObject(res.flag)) { setIfString(span, SEMATTRS_AUTUMN_FLAG_ID, res.flag.id); - setIfString(span, SEMATTRS_AUTUMN_PLAN_ID, res.flag.planId); + setIfString(span, SEMATTRS_AUTUMN_PLAN_ID, res.flag.planId ?? res.flag.product_id); } span.setAttribute(SEMATTRS_AUTUMN_HAS_PREVIEW, Boolean(res.preview)); if (isObject(res.preview)) { @@ -225,10 +240,10 @@ const annotateCheckResponse: Annotator = (span, res) => { const annotateTrackRequest: Annotator = (span, req) => { if (!isObject(req)) return; - setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, req.customerId); - setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, req.featureId); - setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, req.entityId); - setIfString(span, SEMATTRS_AUTUMN_EVENT_NAME, req.eventName); + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, req.customerId ?? req.customer_id); + setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, req.featureId ?? req.feature_id); + setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, req.entityId ?? req.entity_id); + setIfString(span, SEMATTRS_AUTUMN_EVENT_NAME, req.eventName ?? req.event_name); setIfNumber(span, SEMATTRS_AUTUMN_VALUE, req.value); if (isObject(req.lock)) { setIfString(span, SEMATTRS_AUTUMN_LOCK, req.lock.lockId); @@ -237,13 +252,14 @@ const annotateTrackRequest: Annotator = (span, req) => { const annotateTrackResponse: Annotator = (span, res) => { if (!isObject(res)) return; - setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId); - setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, res.entityId); - setIfString(span, SEMATTRS_AUTUMN_EVENT_NAME, res.eventName); + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId ?? res.customer_id); + setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, res.entityId ?? res.entity_id); + setIfString(span, SEMATTRS_AUTUMN_EVENT_NAME, res.eventName ?? res.event_name); + setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, res.featureId ?? res.feature_id); setIfNumber(span, SEMATTRS_AUTUMN_VALUE, res.value); if (isObject(res.balance)) { setIfNumber(span, SEMATTRS_AUTUMN_BALANCE, res.balance.remaining); - setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, res.balance.featureId); + setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, res.balance.featureId ?? res.balance.feature_id); } if (isObject(res.balances)) { span.setAttribute(SEMATTRS_AUTUMN_BALANCE_COUNT, Object.keys(res.balances).length); @@ -254,9 +270,9 @@ const annotateTrackResponse: Annotator = (span, res) => { const annotateAttachRequest: Annotator = (span, req) => { if (!isObject(req)) return; - setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, req.customerId); - setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, req.entityId); - setIfString(span, SEMATTRS_AUTUMN_PLAN_ID, req.planId); + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, req.customerId ?? req.customer_id); + setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, req.entityId ?? req.entity_id); + setIfString(span, SEMATTRS_AUTUMN_PLAN_ID, req.planId ?? req.product_id); setIfString(span, SEMATTRS_AUTUMN_SUBSCRIPTION_ID, req.subscriptionId); setIfBoolean(span, SEMATTRS_AUTUMN_INVOICE_MODE, readEnabled(req.invoiceMode)); setIfString(span, SEMATTRS_AUTUMN_PRORATION_BEHAVIOR, req.prorationBehavior); @@ -273,6 +289,13 @@ const annotateAttachRequest: Annotator = (span, req) => { if (Array.isArray(req.discounts)) { span.setAttribute(SEMATTRS_AUTUMN_DISCOUNT_COUNT, req.discounts.length); } + if (Array.isArray(req.product_ids)) { + const ids = req.product_ids.filter((id): id is string => typeof id === "string"); + if (ids.length > 0) { + span.setAttribute(SEMATTRS_AUTUMN_PLAN_IDS, ids.join(",")); + span.setAttribute(SEMATTRS_AUTUMN_PLAN_COUNT, ids.length); + } + } }; const annotateMultiAttachRequest: Annotator = (span, req) => { @@ -312,28 +335,42 @@ const annotateUpdateRequest: Annotator = (span, req) => { const annotateBillingCustomerRequest: Annotator = (span, req) => { if (!isObject(req)) return; - setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, req.customerId); - setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, req.entityId); + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, req.customerId ?? req.customer_id); + setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, req.entityId ?? req.entity_id); }; const annotateAttachResponse: Annotator = (span, res, config) => { if (!isObject(res)) return; - setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId); - setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, res.entityId); + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId ?? res.customer_id); + setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, res.entityId ?? res.entity_id); if (isObject(res.invoice)) { setIfString(span, SEMATTRS_AUTUMN_INVOICE_ID, res.invoice.stripeId); setIfString(span, SEMATTRS_AUTUMN_INVOICE_STATUS, res.invoice.status); setIfNumber(span, SEMATTRS_AUTUMN_TOTAL_AMOUNT, res.invoice.total); setIfString(span, SEMATTRS_AUTUMN_CURRENCY, res.invoice.currency); } - const hasUrl = typeof res.paymentUrl === "string" && res.paymentUrl.length > 0; - span.setAttribute(SEMATTRS_AUTUMN_HAS_PAYMENT_URL, hasUrl); - if (hasUrl && config.captureCustomerData) { - setIfString(span, SEMATTRS_AUTUMN_PAYMENT_URL, res.paymentUrl); + // 1.x uses `paymentUrl`; pre-1.0 uses `checkout_url`. Both map to the + // same payment-url semantics, gated behind captureCustomerData. + const paymentUrl = typeof res.paymentUrl === "string" && res.paymentUrl.length > 0 + ? res.paymentUrl + : typeof res.checkout_url === "string" && res.checkout_url.length > 0 + ? res.checkout_url + : undefined; + span.setAttribute(SEMATTRS_AUTUMN_HAS_PAYMENT_URL, Boolean(paymentUrl)); + if (paymentUrl && config.captureCustomerData) { + span.setAttribute(SEMATTRS_AUTUMN_PAYMENT_URL, paymentUrl); } if (isObject(res.requiredAction)) { setIfString(span, SEMATTRS_AUTUMN_REQUIRED_ACTION, res.requiredAction.code); } + // Pre-1.0 AttachResult carries an array of product_ids; surface as plan_ids. + if (Array.isArray(res.product_ids)) { + const ids = res.product_ids.filter((id): id is string => typeof id === "string"); + if (ids.length > 0) { + span.setAttribute(SEMATTRS_AUTUMN_PLAN_IDS, ids.join(",")); + span.setAttribute(SEMATTRS_AUTUMN_PLAN_COUNT, ids.length); + } + } }; const annotatePreviewResponse: Annotator = (span, res) => { @@ -346,7 +383,7 @@ const annotatePreviewResponse: Annotator = (span, res) => { const annotatePortalResponse: Annotator = (span, res, config) => { if (!isObject(res)) return; - setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId); + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId ?? res.customer_id); const url = typeof res.url === "string" ? res.url : undefined; span.setAttribute(SEMATTRS_AUTUMN_HAS_PORTAL_URL, Boolean(url)); if (url && config.captureCustomerData) { @@ -356,7 +393,7 @@ const annotatePortalResponse: Annotator = (span, res, config) => { const annotateSetupPaymentResponse: Annotator = (span, res, config) => { if (!isObject(res)) return; - setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId); + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId ?? res.customer_id); const url = typeof res.url === "string" ? res.url : undefined; span.setAttribute(SEMATTRS_AUTUMN_HAS_PAYMENT_URL, Boolean(url)); if (url && config.captureCustomerData) { @@ -364,6 +401,41 @@ const annotateSetupPaymentResponse: Annotator = (span, res, config) => { } }; +// ---------- Pre-1.0 flat-method annotators ---------- + +const annotateCancelRequest: Annotator = (span, req) => { + if (!isObject(req)) return; + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, req.customerId ?? req.customer_id); + setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, req.entityId ?? req.entity_id); + setIfString(span, SEMATTRS_AUTUMN_PLAN_ID, req.planId ?? req.product_id); + if (typeof req.cancel_immediately === "boolean") { + span.setAttribute( + SEMATTRS_AUTUMN_CANCEL_ACTION, + req.cancel_immediately ? "cancel_immediately" : "cancel_end_of_cycle", + ); + } +}; + +const annotateCancelResponse: Annotator = (span, res) => { + if (!isObject(res)) return; + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId ?? res.customer_id); + setIfString(span, SEMATTRS_AUTUMN_PLAN_ID, res.planId ?? res.product_id); +}; + +const annotateUsageRequest: Annotator = (span, req) => { + if (!isObject(req)) return; + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, req.customerId ?? req.customer_id); + setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, req.featureId ?? req.feature_id); + setIfString(span, SEMATTRS_AUTUMN_ENTITY_ID, req.entityId ?? req.entity_id); + setIfNumber(span, SEMATTRS_AUTUMN_VALUE, req.value); +}; + +const annotateUsageResponse: Annotator = (span, res) => { + if (!isObject(res)) return; + setIfString(span, SEMATTRS_AUTUMN_CUSTOMER_ID, res.customerId ?? res.customer_id); + setIfString(span, SEMATTRS_AUTUMN_FEATURE_ID, res.featureId ?? res.feature_id); +}; + // ---------- Customers annotators ---------- const annotateCustomerRequest: Annotator = (span, req) => { @@ -565,7 +637,7 @@ function wrapAsyncMethod( const result = await context.with(activeContext, () => originalMethod.apply(this, args)); if (config.captureResponseAttributes !== false && responseAnnotator) { try { - responseAnnotator(span, result, config); + responseAnnotator(span, unwrapResult(result), config); } catch { // swallow } @@ -709,7 +781,7 @@ const SUB_RESOURCES: SubResource[] = [ function wrapTopLevel( client: Autumn, - operationName: "check" | "track", + operationName: "check" | "track" | "attach" | "cancel" | "setupPayment" | "usage", tracer: Tracer, config: InstrumentAutumnConfig, requestAnnotator: Annotator, @@ -761,6 +833,15 @@ export function instrumentAutumn( wrapTopLevel(client, "check", tracer, config, annotateCheckRequest, annotateCheckResponse); wrapTopLevel(client, "track", tracer, config, annotateTrackRequest, annotateTrackResponse); + // Pre-1.0 autumn-js exposed billing flows as flat top-level methods. In 1.x + // these live under `autumn.billing.*` (or were replaced, e.g. `cancel` → + // `billing.update({ cancelAction })`), so the Autumn class doesn't expose + // them and each wrap is a no-op (wrapTopLevel checks typeof before wrapping). + wrapTopLevel(client, "attach", tracer, config, annotateAttachRequest, annotateAttachResponse); + wrapTopLevel(client, "cancel", tracer, config, annotateCancelRequest, annotateCancelResponse); + wrapTopLevel(client, "setupPayment", tracer, config, annotateBillingCustomerRequest, annotateSetupPaymentResponse); + wrapTopLevel(client, "usage", tracer, config, annotateUsageRequest, annotateUsageResponse); + for (const sub of SUB_RESOURCES) { if (config[sub.flag] === false) continue; let resource: unknown; diff --git a/packages/otel-drizzle/examples/README.md b/packages/otel-drizzle/examples/README.md new file mode 100644 index 0000000..bd38426 --- /dev/null +++ b/packages/otel-drizzle/examples/README.md @@ -0,0 +1,117 @@ +# otel-drizzle trace playground + +Replays representative queries across **every instrumentation surface** that `@api-blitz/otel-drizzle` supports and exports the spans via OTLP/HTTP to a local Jaeger. Use this to visually verify span names, `db.*` attributes, transaction markers, error handling, and long-query truncation. + +## Run it + +From the repo root: + +```bash +docker compose up -d # starts Jaeger on :16686 and :4318 +pnpm --filter @api-blitz/otel-drizzle install # first time only +pnpm --filter @api-blitz/otel-drizzle example +``` + +Then open , pick **Service** `otel-drizzle-demo`, and click **Find Traces**. + +When finished: + +```bash +docker compose down +``` + +## What you should see + +**30 `CLIENT`-kind spans** across 12 distinct SQL operations, including 2 ERROR spans, 3 transaction spans, and 1 truncated-query span. Every span carries `db.system=postgresql`, `db.name=demo`, `net.peer.name=localhost`, `net.peer.port=5432`. + +### Phase 1 — `instrumentDrizzle` with `query` (promise-based) + +17 spans covering every SQL verb the operation extractor recognizes, plus all three query-object shapes, plus truncation and one error. + +| SQL verb | Span name | Input shape | +|---|---|---| +| `SELECT` | `drizzle.select` | string | +| `INSERT` | `drizzle.insert` | string | +| `UPDATE` | `drizzle.update` | string | +| `DELETE` | `drizzle.delete` | string | +| `CREATE` | `drizzle.create` | string | +| `ALTER` | `drizzle.alter` | string | +| `DROP` | `drizzle.drop` | string | +| `TRUNCATE` | `drizzle.truncate` | string | +| `BEGIN` | `drizzle.begin` | string | +| `COMMIT` | `drizzle.commit` | string | +| `SET` | `drizzle.set` | string | +| `WITH` | `drizzle.with` | string | +| `INSERT` | `drizzle.insert` | `{ sql, params }` | +| `UPDATE` | `drizzle.update` | `{ text, values }` | +| `SELECT` | `drizzle.select` | `{ sql, queryChunks }` | +| `SELECT` | `drizzle.select` | long query → `db.statement` truncated to 1000 chars + `...` | +| `SELECT` | `drizzle.select` (ERROR) | `SELECT nonexistent_column FROM users` — promise rejects | + +### Phase 2 — `instrumentDrizzle` with callback pattern (2 spans) + +- `drizzle.select` (OK) — `SELECT id FROM users /* callback-success */` +- `drizzle.select` (ERROR) — `SELECT nonexistent_column FROM users /* callback-error */` + +### Phase 3 — `instrumentDrizzle` with `execute` (2 spans) + +- `drizzle.select` — `SELECT * FROM users WHERE id = $1 /* via execute */` (string arg) +- `drizzle.delete` — `DELETE FROM users WHERE id = $1 /* via execute */` (`{ sql, args }` arg) + +### Phase 4 — `instrumentDrizzleClient` session: `prepareQuery` + `query` (3 spans) + +Exercises the Drizzle query-builder path (`db.select().from()` internally calls `session.prepareQuery(...).execute()`): + +- `drizzle.select` — prepared `SELECT * FROM users /* prepared */` +- `drizzle.insert` — prepared `INSERT INTO users (name) VALUES ($1) /* prepared */` +- `drizzle.update` — direct `session.query("UPDATE users SET name = $1 /* direct session */", ["Ada"])` + +### Phase 5 — `instrumentDrizzleClient` transaction (3 spans, each with `db.transaction=true`) + +Exercises `session.transaction(tx => ...)` where the callback calls `tx.execute(...)`: + +- `drizzle.set` — `SET LOCAL role org_role /* in tx */` +- `drizzle.select` — `SELECT set_config('request.org_id', $1, true) /* in tx */` +- `drizzle.insert` — `INSERT INTO org_audit (action) VALUES ($1) /* in tx */` + +### Phase 6 — `instrumentDrizzleClient` with `$client` fallback (1 span) + +Exercises the fallback path when `db` has a `$client` property but no `session` (drizzle postgres-js pattern): + +- `drizzle.select` — `SELECT id FROM users /* via $client */` + +### Phase 7 — `instrumentDrizzleClient` with `_.session.execute` fallback (2 spans) + +Exercises the last-resort fallback when `db._.session.execute` is the only instrumentable surface: + +- `drizzle.insert` — `INSERT INTO users (name) VALUES ($1) /* via _.session */` +- `drizzle.delete` — `DELETE FROM users WHERE id = $1 /* via _.session */` + +## Quick Jaeger API checks + +```bash +# Expect 12 unique operations +curl -s 'http://localhost:16686/api/services/otel-drizzle-demo/operations' \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['total'])" + +# Expect 30 total spans, 2 error spans, 3 transaction spans +curl -s 'http://localhost:16686/api/traces?service=otel-drizzle-demo&limit=200' \ + | python3 -c " +import sys,json +d=json.load(sys.stdin) +total=err=tx=0 +for t in d['data']: + for s in t['spans']: + total += 1 + tags={x['key']:x.get('value') for x in s.get('tags',[])} + if tags.get('error')==True: err += 1 + if tags.get('db.transaction')==True: tx += 1 +print(f'{total} spans, {err} errors, {tx} in tx')" +``` + +## Notes + +- Every surface is exercised via **plain-object mocks** (same pattern as the unit tests), so no real Postgres is required. +- The demo runs with `dbSystem=postgresql`, `dbName=demo`, `peerName=localhost`, `peerPort=5432` so you can verify those attributes appear on every span. +- Default `maxQueryTextLength=1000`; the long-query span demonstrates truncation with a trailing `...`. +- OTLP endpoint defaults to `http://localhost:4318`; override with `OTEL_EXPORTER_OTLP_ENDPOINT` if Jaeger runs elsewhere. diff --git a/packages/otel-drizzle/examples/demo.ts b/packages/otel-drizzle/examples/demo.ts new file mode 100644 index 0000000..b12d6d1 --- /dev/null +++ b/packages/otel-drizzle/examples/demo.ts @@ -0,0 +1,263 @@ +import { instrumentDrizzle, instrumentDrizzleClient } from "../src/index"; +import { setupOtel } from "./otel-setup"; + +type QueryFn = (...args: any[]) => unknown; + +function extractText(q: unknown): string { + if (typeof q === "string") return q; + if (q && typeof q === "object") { + const o = q as { sql?: unknown; text?: unknown }; + if (typeof o.sql === "string") return o.sql; + if (typeof o.text === "string") return o.text; + } + return ""; +} + +/** Mock 1: a pg-style client with both `query` and `execute` methods. */ +function createMockQueryClient(): { query: QueryFn; execute?: QueryFn } { + return { + query: (q: unknown, secondArg?: unknown) => { + const text = extractText(q); + // Callback-based pattern: last arg is a function + if (typeof secondArg === "function") { + const cb = secondArg as (err: unknown, res: unknown) => void; + if (text.includes("nonexistent_column")) { + cb(new Error("demo: column does not exist"), null); + return undefined; + } + cb(null, { rows: [{ id: 1 }] }); + return undefined; + } + // Promise-based + return (async () => { + if (text.includes("nonexistent_column")) { + throw new Error("demo: column does not exist"); + } + return { rows: [{ id: 1 }] }; + })(); + }, + }; +} + +/** Mock 2: a libsql-style client that only has `execute`. */ +function createMockExecuteClient(): { execute: QueryFn } { + return { + execute: async (q: unknown) => { + const _text = extractText(q); + return { rows: [{ id: 1 }] }; + }, + }; +} + +/** Mock 3: a db shape with session.prepareQuery (drizzle pg pattern). */ +function createMockPrepareQueryDb() { + const session = { + prepareQuery: (queryObj: { sql: string }) => ({ + execute: async () => ({ rows: [{ id: 1 }] }), + _queryObj: queryObj, + }), + query: async (_sql: string, _params: unknown[]) => ({ rows: [{ id: 1 }] }), + transaction: async (cb: (tx: unknown) => unknown) => { + const tx: any = { + execute: async (_q: unknown) => ({ rows: [{ id: 1 }] }), + }; + return cb(tx); + }, + }; + return { session, select: () => undefined }; +} + +/** Mock 4: a db shape with $client (drizzle postgres-js pattern). */ +function createMockDollarClientDb() { + return { + $client: createMockQueryClient(), + select: () => undefined, + }; +} + +/** Mock 5: a db shape with _.session.execute (drizzle mysql2 pattern). */ +function createMockUnderscoreSessionDb() { + return { + _: { + session: { + execute: async (_q: unknown) => ({ rows: [{ id: 1 }] }), + }, + }, + select: () => undefined, + }; +} + +async function main() { + const provider = setupOtel("otel-drizzle-demo"); + + const config = { + dbSystem: "postgresql", + dbName: "demo", + peerName: "localhost", + peerPort: 5432, + }; + + // ============================================================ + // Phase 1 — instrumentDrizzle with `query` (promise-based) + // All SQL verbs + long-query truncation + error path + // ============================================================ + const q1 = instrumentDrizzle(createMockQueryClient(), config); + + // SQL verbs as plain strings + await q1.query("SELECT id, email FROM users WHERE org_id = $1"); + await q1.query("INSERT INTO events (kind, payload) VALUES ($1, $2)"); + await q1.query("UPDATE users SET last_seen_at = now() WHERE id = $1"); + await q1.query("DELETE FROM sessions WHERE expires_at < now()"); + await q1.query("CREATE TABLE audits (id serial primary key, action text)"); + await q1.query("ALTER TABLE audits ADD COLUMN actor_id text"); + await q1.query("DROP TABLE audits"); + await q1.query("TRUNCATE TABLE events"); + await q1.query("BEGIN"); + await q1.query("COMMIT"); + await q1.query("SET LOCAL search_path TO public"); + await q1.query( + "WITH recent AS (SELECT * FROM events ORDER BY created_at DESC LIMIT 10) SELECT * FROM recent", + ); + + // Query-object shapes + await q1.query({ + sql: "INSERT INTO events (kind, payload) VALUES ($1, $2)", + params: ["signup", { email: "ada@example.com" }], + }); + await q1.query({ + text: "UPDATE users SET last_seen_at = now() WHERE id = $1", + values: [1], + }); + await q1.query({ + sql: "SELECT * FROM users LIMIT 1", + queryChunks: ["SELECT * FROM users LIMIT 1"], + }); + + // Long query truncation (default maxQueryTextLength=1000 → appends "...") + const longQuery = `SELECT ${"col_x, ".repeat(200)}id FROM big_table`; + await q1.query(longQuery); + + // Error path (promise reject) + try { + await q1.query("SELECT nonexistent_column FROM users"); + } catch { + // expected — ERROR span + } + + // ============================================================ + // Phase 2 — instrumentDrizzle with callback pattern + // ============================================================ + const q2 = instrumentDrizzle(createMockQueryClient(), config); + + await new Promise((resolve) => { + q2.query("SELECT id FROM users /* callback-success */", () => resolve()); + }); + + await new Promise((resolve) => { + q2.query("SELECT nonexistent_column FROM users /* callback-error */", () => + resolve(), + ); + }); + + // ============================================================ + // Phase 3 — instrumentDrizzle with `execute` method + // ============================================================ + const e1 = instrumentDrizzle(createMockExecuteClient(), config); + await e1.execute?.("SELECT * FROM users WHERE id = $1 /* via execute */"); + await e1.execute?.({ + sql: "DELETE FROM users WHERE id = $1 /* via execute */", + args: [1], + }); + + // ============================================================ + // Phase 4 — instrumentDrizzleClient: session.prepareQuery + session.query + // ============================================================ + const db1 = instrumentDrizzleClient(createMockPrepareQueryDb(), config); + + const preparedSelect = db1.session.prepareQuery({ + sql: "SELECT * FROM users /* prepared */", + }); + await preparedSelect.execute(); + + const preparedInsert = db1.session.prepareQuery({ + sql: "INSERT INTO users (name) VALUES ($1) /* prepared */", + }); + await preparedInsert.execute(); + + await db1.session.query("UPDATE users SET name = $1 /* direct session */", [ + "Ada", + ]); + + // ============================================================ + // Phase 5 — instrumentDrizzleClient: transaction path (tx.execute) + // Verify db.transaction=true attribute is set + // ============================================================ + await db1.session.transaction(async (tx: any) => { + await tx.execute({ sql: "SET LOCAL role org_role /* in tx */" }); + await tx.execute({ + sql: "SELECT set_config('request.org_id', $1, true) /* in tx */", + params: ["org_demo"], + }); + await tx.execute({ + sql: "INSERT INTO org_audit (action) VALUES ($1) /* in tx */", + params: ["demo"], + }); + }); + + // ============================================================ + // Phase 6 — instrumentDrizzleClient: $client fallback + // ============================================================ + const db2 = instrumentDrizzleClient(createMockDollarClientDb(), config); + await (db2.$client as { query: QueryFn }).query( + "SELECT id FROM users /* via $client */", + ); + + // ============================================================ + // Phase 7 — instrumentDrizzleClient: _.session.execute fallback + // ============================================================ + const db3 = instrumentDrizzleClient(createMockUnderscoreSessionDb(), config); + await db3._.session.execute?.( + "INSERT INTO users (name) VALUES ($1) /* via _.session */", + ); + await db3._.session.execute?.( + "DELETE FROM users WHERE id = $1 /* via _.session */", + ); + + try { + await provider.forceFlush(); + await provider.shutdown(); + } catch (err) { + if (isConnRefused(err)) { + console.error( + "Could not reach the OTLP endpoint at http://localhost:4318. Is Jaeger running? Try `docker compose up -d` from the repo root.", + ); + process.exit(1); + } + throw err; + } + + console.log( + "Demo complete. Open http://localhost:16686 and pick service 'otel-drizzle-demo'.", + ); +} + +function isConnRefused(err: unknown): boolean { + if (!err || typeof err !== "object") return false; + if (Array.isArray(err)) return err.some(isConnRefused); + const e = err as { code?: string; errors?: unknown[]; cause?: unknown }; + if (e.code === "ECONNREFUSED") return true; + if (Array.isArray(e.errors) && e.errors.some(isConnRefused)) return true; + if (e.cause && isConnRefused(e.cause)) return true; + return false; +} + +main().catch((err) => { + if (isConnRefused(err)) { + console.error( + "Could not reach the OTLP endpoint at http://localhost:4318. Is Jaeger running? Try `docker compose up -d` from the repo root.", + ); + process.exit(1); + } + console.error(err); + process.exit(1); +}); diff --git a/packages/otel-drizzle/examples/otel-setup.ts b/packages/otel-drizzle/examples/otel-setup.ts new file mode 100644 index 0000000..c8a3bdc --- /dev/null +++ b/packages/otel-drizzle/examples/otel-setup.ts @@ -0,0 +1,28 @@ +import { trace } from "@opentelemetry/api"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; +import { resourceFromAttributes } from "@opentelemetry/resources"; +import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; +import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; + +const JAEGER_OTLP_URL = + process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.replace(/\/$/, "") ?? + "http://localhost:4318"; + +export function setupOtel(serviceName: string): NodeTracerProvider { + const provider = new NodeTracerProvider({ + resource: resourceFromAttributes({ + [ATTR_SERVICE_NAME]: serviceName, + }), + spanProcessors: [ + new BatchSpanProcessor( + new OTLPTraceExporter({ + url: `${JAEGER_OTLP_URL}/v1/traces`, + timeoutMillis: 3000, + }), + ), + ], + }); + trace.setGlobalTracerProvider(provider); + return provider; +} diff --git a/packages/otel-drizzle/package.json b/packages/otel-drizzle/package.json index cf452fd..028b9cf 100644 --- a/packages/otel-drizzle/package.json +++ b/packages/otel-drizzle/package.json @@ -31,6 +31,7 @@ "scripts": { "build": "pnpm clean && tsc", "clean": "rimraf dist", + "example": "tsx examples/demo.ts", "prepublishOnly": "pnpm build", "type-check": "tsc --noEmit", "unit-test": "vitest --run", @@ -38,12 +39,17 @@ }, "devDependencies": { "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", + "@opentelemetry/resources": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.1.0", + "@opentelemetry/sdk-trace-node": "^2.1.0", + "@opentelemetry/semantic-conventions": "^1.37.0", "@types/node": "18.15.11", "@types/pg": "^8.11.10", "drizzle-orm": "^0.36.4", "postgres": "^3.4.7", "rimraf": "3.0.2", + "tsx": "^4.19.2", "typescript": "^5", "vitest": "0.33.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4398c9e..6df1d73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,21 @@ importers: '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^2.1.0 + version: 2.1.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': specifier: ^2.1.0 version: 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^2.1.0 + version: 2.7.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.37.0 + version: 1.37.0 '@types/node': specifier: 18.15.11 version: 18.15.11 @@ -38,6 +50,9 @@ importers: rimraf: specifier: 3.0.2 version: 3.0.2 + tsx: + specifier: ^4.19.2 + version: 4.21.0 typescript: specifier: ^5 version: 5.3.3 @@ -50,9 +65,21 @@ importers: '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^2.1.0 + version: 2.1.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': specifier: ^2.1.0 version: 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^2.1.0 + version: 2.7.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.37.0 + version: 1.37.0 '@types/node': specifier: 18.15.11 version: 18.15.11 @@ -68,6 +95,9 @@ importers: rimraf: specifier: 3.0.2 version: 3.0.2 + tsx: + specifier: ^4.19.2 + version: 4.21.0 typescript: specifier: ^5 version: 5.3.3 @@ -163,138 +193,294 @@ packages: '@changesets/write@0.3.0': resolution: {integrity: sha512-slGLb21fxZVUYbyea+94uFiD6ntQW0M2hIKNznFizDhZPDgn2c/fv1UzzlW43RVzh1BEDuIqW6hzlJ1OflNmcw==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -320,32 +506,144 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api-logs@0.203.0': + resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@opentelemetry/context-async-hooks@2.7.0': + resolution: {integrity: sha512-MWXggArM+Y11mPS8VOrqxOj+YMGQSRuvhM91eSBX4xFpJa05mpkeVvM8pPux5ElkEjV5RMgrkisrlP/R83SpBQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.0.1': + resolution: {integrity: sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.1.0': resolution: {integrity: sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.7.0': + resolution: {integrity: sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-trace-otlp-http@0.203.0': + resolution: {integrity: sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.203.0': + resolution: {integrity: sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.203.0': + resolution: {integrity: sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.0.1': + resolution: {integrity: sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.1.0': resolution: {integrity: sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.7.0': + resolution: {integrity: sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.203.0': + resolution: {integrity: sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.0.1': + resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.0.1': + resolution: {integrity: sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.1.0': resolution: {integrity: sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.7.0': + resolution: {integrity: sha512-Yg9zEXJB50DLVLpsKPk7NmNqlPlS+OvqhJGh0A8oawIOTPOwlm4eXs9BMJV7L79lvEwI+dWtAj+YjTyddV336A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.7.0': + resolution: {integrity: sha512-RrFHOXw0IYp/OThew6QORdybnnLitUAUMCJKcQNBYS0hDkCYarO2vTkVxfrGxCIqd5XHSMvbCpBd/T8ZMw8oSg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.37.0': resolution: {integrity: sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==} engines: {node: '>=14'} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -777,6 +1075,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -866,6 +1169,9 @@ packages: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1116,6 +1422,9 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1363,6 +1672,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + protobufjs@7.5.5: + resolution: {integrity: sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==} + engines: {node: '>=12.0.0'} + prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} @@ -1425,6 +1738,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -1643,6 +1959,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tty-table@4.2.3: resolution: {integrity: sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA==} engines: {node: '>=8.0.0'} @@ -2078,72 +2399,150 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@esbuild/aix-ppc64@0.27.7': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + '@esbuild/android-arm@0.18.20': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + '@esbuild/android-x64@0.18.20': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true + '@esbuild/linux-arm64@0.27.7': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true + '@esbuild/linux-arm@0.27.7': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 @@ -2178,19 +2577,95 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.16.0 + '@opentelemetry/api-logs@0.203.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api@1.9.0': {} + '@opentelemetry/context-async-hooks@2.7.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/core@2.7.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.5 + + '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/resources@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/resources@2.7.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/sdk-logs@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -2198,8 +2673,45 @@ snapshots: '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/sdk-trace-base@2.7.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + + '@opentelemetry/sdk-trace-node@2.7.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.7.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions@1.37.0': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@sinclair/typebox@0.27.8': {} '@types/chai-subset@1.3.5': @@ -2629,6 +3141,35 @@ snapshots: '@esbuild/win32-ia32': 0.18.20 '@esbuild/win32-x64': 0.18.20 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.1.1: {} escape-string-regexp@1.0.5: {} @@ -2724,6 +3265,10 @@ snapshots: call-bind: 1.0.5 get-intrinsic: 1.2.2 + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2964,6 +3509,8 @@ snapshots: lodash.startcase@4.4.0: {} + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -3197,6 +3744,21 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.2.0 + protobufjs@7.5.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 18.15.11 + long: 5.3.2 + prr@1.0.1: optional: true @@ -3263,6 +3825,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.8: dependencies: is-core-module: 2.13.1 @@ -3488,6 +4052,13 @@ snapshots: tslib@2.8.1: optional: true + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + tty-table@4.2.3: dependencies: chalk: 4.1.2 From 3f2dd12edc97e22a59b64847886f090a13ea79ea Mon Sep 17 00:00:00 2001 From: ibadus Date: Fri, 24 Apr 2026 13:15:05 +0200 Subject: [PATCH 2/2] chore: changeset for otel-autumn 1.1.0 Minor bump for autumn-js pre-1.0 compatibility (widened peer range, flat top-level method wrapping, Result envelope unwrapping, plan id attribute mapping). Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/autumn-js-pre-1-compat.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/autumn-js-pre-1-compat.md diff --git a/.changeset/autumn-js-pre-1-compat.md b/.changeset/autumn-js-pre-1-compat.md new file mode 100644 index 0000000..bd5fa84 --- /dev/null +++ b/.changeset/autumn-js-pre-1-compat.md @@ -0,0 +1,10 @@ +--- +"@api-blitz/otel-autumn": minor +--- + +Add `autumn-js` pre-1.0 compatibility. + +- Widen `autumn-js` peer range to `>=0.0.70 <2.0.0`. +- Wrap pre-1.0 flat top-level methods (`attach`, `cancel`, `setupPayment`, `usage`) alongside the existing 1.x sub-resource coverage; methods missing from the installed SDK are skipped silently. +- Unwrap the pre-1.0 `Result` response envelope so response-side span attributes populate the same on both versions. +- Map `product_id`/`product_ids` to `autumn.plan_id`/`autumn.plan_ids` for dashboard consistency across versions.