@cross-deck/node 1.8.2 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +22 -0
- package/dist/auto-events/index.cjs +23 -0
- package/dist/auto-events/index.cjs.map +1 -1
- package/dist/auto-events/index.mjs +23 -0
- package/dist/auto-events/index.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.9.0] — 2026-06-24
|
|
10
|
+
|
|
11
|
+
**Added — read-cost cross-match (the Buckets bridge).** When
|
|
12
|
+
[`@cross-deck/buckets`](https://www.npmjs.com/package/@cross-deck/buckets) is
|
|
13
|
+
installed alongside this SDK, the auto-events adapters now tell it **who** and
|
|
14
|
+
**what** each request is, so every database read inside the request attributes to
|
|
15
|
+
the identified user *and* the operation that spent it — the cross-match no
|
|
16
|
+
standalone read profiler can do.
|
|
17
|
+
|
|
18
|
+
- `crossdeckExpress` stamps the request's `developerUserId` (from your
|
|
19
|
+
`getIdentity`) as the read-cost **actor** at request entry, before the route
|
|
20
|
+
handler issues a single read.
|
|
21
|
+
- `wrapLambdaHandler` stamps the **actor** *and* the **function name** as the
|
|
22
|
+
operation (the function is the unit on serverless).
|
|
23
|
+
|
|
24
|
+
Decoupled and zero-dependency: the SDK never imports Buckets — it drives a global
|
|
25
|
+
bridge (`__crossdeckBucketsBridge__`). With no collector installed, every call is
|
|
26
|
+
a silent no-op, so this is safe whether a customer runs the SDK alone, Buckets
|
|
27
|
+
alone, or both. WHAT on the server is otherwise named by your own `bucket()` tags.
|
|
28
|
+
|
|
9
29
|
## [1.8.2] — 2026-06-22
|
|
10
30
|
|
|
11
31
|
**Docs.** Reverted the read-cost dashboard preview 1.8.1 added to the README —
|
package/README.md
CHANGED
|
@@ -213,6 +213,28 @@ For test fixtures that need to mint signed webhooks against the same scheme, `si
|
|
|
213
213
|
|
|
214
214
|
## Cross-cutting
|
|
215
215
|
|
|
216
|
+
### Read-cost cross-match (Buckets)
|
|
217
|
+
|
|
218
|
+
Install [`@cross-deck/buckets`](https://www.npmjs.com/package/@cross-deck/buckets)
|
|
219
|
+
alongside this SDK and the auto-events adapters wire the **cross-match** for you:
|
|
220
|
+
every database read inside a request attributes to the **user** who triggered it and
|
|
221
|
+
the **operation** that spent it — the thing a standalone query profiler can't tell
|
|
222
|
+
you, because it doesn't know your users.
|
|
223
|
+
|
|
224
|
+
- `crossdeckExpress` stamps the request's `developerUserId` (from your `getIdentity`)
|
|
225
|
+
as the read-cost actor, at request entry — before the route handler runs.
|
|
226
|
+
- `wrapLambdaHandler` stamps the actor *and* the function name as the operation.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
app.use(crossdeckExpress(server, {
|
|
230
|
+
getIdentity: (req) => ({ developerUserId: req.user?.id }), // WHO — also drives reads
|
|
231
|
+
}));
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
No new dependency and no import of Buckets — the SDK drives a global bridge, so a
|
|
235
|
+
missing collector is a silent no-op. Name the heavy server operations with `bucket()`
|
|
236
|
+
from Buckets; read it all back with `npx @cross-deck/buckets` or in the dashboard.
|
|
237
|
+
|
|
216
238
|
### Runtime info
|
|
217
239
|
|
|
218
240
|
Auto-detected at construction. Attached to every event + error as `runtime.*` properties:
|
|
@@ -29,6 +29,16 @@ __export(auto_events_exports, {
|
|
|
29
29
|
});
|
|
30
30
|
module.exports = __toCommonJS(auto_events_exports);
|
|
31
31
|
|
|
32
|
+
// src/read-cost-bridge.ts
|
|
33
|
+
var BUCKETS_BRIDGE_KEY = "__crossdeckBucketsBridge__";
|
|
34
|
+
function bridgeReadCost(ctx) {
|
|
35
|
+
try {
|
|
36
|
+
const setter = globalThis[BUCKETS_BRIDGE_KEY];
|
|
37
|
+
if (typeof setter === "function") setter(ctx);
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
// src/auto-events/express.ts
|
|
33
43
|
var DEFAULT_SKIP_PATHS = [/^\/crossdeck($|\/)/];
|
|
34
44
|
function crossdeckExpress(server, options = {}) {
|
|
@@ -40,6 +50,15 @@ function crossdeckExpress(server, options = {}) {
|
|
|
40
50
|
}
|
|
41
51
|
const start = Date.now();
|
|
42
52
|
let dispatched = false;
|
|
53
|
+
try {
|
|
54
|
+
let actor;
|
|
55
|
+
try {
|
|
56
|
+
actor = options.getIdentity?.(req)?.developerUserId;
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
if (actor) bridgeReadCost({ actor });
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
43
62
|
const emit = () => {
|
|
44
63
|
if (dispatched) return;
|
|
45
64
|
dispatched = true;
|
|
@@ -143,6 +162,10 @@ function wrapLambdaHandler(server, handler, options = {}) {
|
|
|
143
162
|
const coldStart = containerColdStart;
|
|
144
163
|
containerColdStart = false;
|
|
145
164
|
const identity = safeExtractIdentity(options.getIdentity, event, context);
|
|
165
|
+
try {
|
|
166
|
+
bridgeReadCost({ actor: identity?.developerUserId, feature: context.functionName });
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
146
169
|
server.track({
|
|
147
170
|
name: "function.invoked",
|
|
148
171
|
developerUserId: identity?.developerUserId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auto-events/index.ts","../../src/auto-events/express.ts","../../src/auto-events/lambda.ts","../../src/auto-events/firebase.ts"],"sourcesContent":["/**\n * @cross-deck/node/auto-events — framework adapters barrel.\n *\n * Each adapter is a thin wrapper around the core `CrossdeckServer`\n * API. They don't pull their framework's runtime types as a hard\n * dependency — the imports are shape-only — so a customer using\n * Express but not Firebase doesn't pay for `firebase-functions`\n * types, and vice versa.\n *\n * The three adapters approved for v1.0.0 (per the SDK_TRUTH.md\n * capability matrix Q1 decision):\n * - Express + ExpressErrorHandler — `request.handled` + route-error capture\n * - Lambda — `function.invoked` / `completed` / `failed` + flush-before-return\n * - Firebase / Cloud Run — generic `wrapFunction` for any handler shape\n *\n * Fastify is deferred to v0.3.0 (Q1 decision). Cloudflare Workers and\n * Vercel Edge are deferred to v0.4+ (no `process.on(...)` lifecycle\n * in Workers — flush-on-exit needs a runtime-specific pattern).\n */\n\nexport {\n crossdeckExpress,\n crossdeckExpressErrorHandler,\n shouldSkipRequest,\n extractRoutePattern,\n} from \"./express\";\nexport type {\n CrossdeckExpressOptions,\n ExpressNext,\n ExpressRequestLike,\n ExpressResponseLike,\n} from \"./express\";\n\nexport { wrapLambdaHandler } from \"./lambda\";\nexport type {\n LambdaContextLike,\n LambdaHandlerLike,\n WrapLambdaOptions,\n} from \"./lambda\";\n\nexport { wrapFunction } from \"./firebase\";\nexport type { WrapFunctionOptions, WrapFunctionMetadata } from \"./firebase\";\n","/**\n * Express auto-events — `request.handled` middleware + uncaught-route\n * error capture.\n *\n * Two middleware factories, registered separately because Express\n * differentiates them by arity:\n *\n * import { crossdeckExpress, crossdeckExpressErrorHandler } from\n * \"@cross-deck/node/auto-events\";\n *\n * app.use(crossdeckExpress(server)); // request middleware\n * app.use(routes); // your routes\n * app.use(crossdeckExpressErrorHandler(server)); // LAST — error middleware\n *\n * `crossdeckExpress` emits `request.handled` on response 'finish'\n * with the matched route pattern (not the full URL — high-cardinality\n * URL paths kill dashboards), method, statusCode, and durationMs.\n *\n * `crossdeckExpressErrorHandler` catches errors thrown in route\n * handlers (sync OR async — Express 5 supports async handlers\n * natively; Express 4 needs the caller to forward via `next(err)`).\n * The error is shipped with request context (url, method, matched\n * route) attached so the dashboard can group by route.\n *\n * Compatible with both Express 4 and Express 5. The middleware\n * signatures are stable across both versions.\n *\n * No `import` from `express`. The adapter speaks shape-only against\n * Express's request / response objects — customers don't pay a\n * forced dependency on Express just to install the Crossdeck SDK.\n * If `express` is missing at install, this module still compiles.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\n\n/**\n * Shape of an Express request object — enough fields for the\n * middleware to do its job without depending on Express's types.\n */\nexport interface ExpressRequestLike {\n method: string;\n url: string;\n path?: string;\n route?: { path?: string | RegExp | Array<string | RegExp> };\n originalUrl?: string;\n headers?: Record<string, string | string[] | undefined>;\n}\n\n/** Shape of an Express response object. */\nexport interface ExpressResponseLike {\n statusCode: number;\n once(event: \"finish\" | \"close\", listener: () => void): unknown;\n /**\n * Optional — Express's response exposes this for reading headers\n * the framework / middleware chain set. Used by the middleware to\n * surface `responseBytes` on the `request.handled` event. If your\n * adapter doesn't have it, the field is simply omitted.\n */\n getHeader?(name: string): string | string[] | number | undefined;\n}\n\nexport type ExpressNext = (err?: unknown) => void;\n\nexport interface CrossdeckExpressOptions {\n /**\n * Routes to skip. Tested against `req.route?.path` if available, else\n * `req.path` / `req.url`. Defaults to a single self-skip for\n * `/crossdeck/*` so the SDK doesn't emit telemetry about its own\n * health endpoints.\n */\n skipPaths?: Array<string | RegExp>;\n /**\n * Optional identity extractor — runs once per request. Whatever it\n * returns is attached to the `request.handled` event so the\n * dashboard can pivot by user. Typical implementation: read\n * `req.user.id` populated by your auth middleware.\n *\n * crossdeckExpress(server, {\n * getIdentity: (req) => ({ developerUserId: req.user?.id }),\n * })\n */\n getIdentity?: (req: ExpressRequestLike) => {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n } | null | undefined;\n /**\n * Attach `{ url, method, route }` as `context.request` on captured\n * errors. Default `true`. Set `false` if you have a separate\n * mechanism for capturing request context (Pino bindings, etc).\n */\n captureErrorsWithRequestContext?: boolean;\n}\n\nconst DEFAULT_SKIP_PATHS: Array<string | RegExp> = [/^\\/crossdeck($|\\/)/];\n\n/**\n * Express middleware that emits `request.handled` per request.\n * Register BEFORE your routes:\n *\n * app.use(crossdeckExpress(server));\n *\n * Behaviour:\n * - Listens on `res.once('finish')` so we capture the FINAL\n * statusCode after any post-route middleware (compression, etc).\n * Also listens on `res.once('close')` to cover client-aborted\n * requests where 'finish' never fires.\n * - Idempotent per request: dispatches once regardless of which\n * terminal event fires first.\n * - `route` property is the matched route PATTERN (`/users/:id`),\n * not the full URL — keeps dashboard cardinality manageable. Falls\n * back to `req.path` when no route matched (404s).\n * - Errors thrown by `getIdentity` are swallowed and the event still\n * ships without identity — telemetry must NEVER break the request\n * pipeline.\n */\nexport function crossdeckExpress(\n server: CrossdeckServer,\n options: CrossdeckExpressOptions = {},\n) {\n const skipPaths = options.skipPaths ?? DEFAULT_SKIP_PATHS;\n\n return function crossdeckExpressMiddleware(\n req: ExpressRequestLike,\n res: ExpressResponseLike,\n next: ExpressNext,\n ): void {\n if (shouldSkipRequest(req, skipPaths)) {\n next();\n return;\n }\n\n const start = Date.now();\n let dispatched = false;\n\n const emit = (): void => {\n if (dispatched) return;\n dispatched = true;\n let identity: ReturnType<NonNullable<CrossdeckExpressOptions[\"getIdentity\"]>> = undefined;\n try {\n identity = options.getIdentity?.(req);\n } catch {\n // identity extraction must never break the request pipeline\n }\n try {\n const props: Record<string, unknown> = {\n route: extractRoutePattern(req),\n method: req.method,\n statusCode: res.statusCode,\n durationMs: Date.now() - start,\n };\n // Defensive: every header read is in a try/swallow because a\n // misbehaving framework middleware may have mutated the\n // response in ways that throw on access.\n try {\n const ua = readHeader(req, \"user-agent\");\n if (ua) props.userAgent = ua;\n } catch {\n // skip\n }\n try {\n const cl = typeof res.getHeader === \"function\" ? res.getHeader(\"content-length\") : undefined;\n if (typeof cl === \"number\") {\n props.responseBytes = cl;\n } else if (typeof cl === \"string\") {\n const parsed = Number(cl);\n if (Number.isFinite(parsed)) props.responseBytes = parsed;\n }\n } catch {\n // skip\n }\n server.track({\n name: \"request.handled\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: props,\n });\n } catch {\n // SDK telemetry must never throw out of the response pipeline.\n }\n };\n\n res.once(\"finish\", emit);\n res.once(\"close\", emit);\n\n next();\n };\n}\n\n/**\n * Express error middleware (4-arg signature). Register LAST, after\n * all routes + after the request middleware:\n *\n * app.use(crossdeckExpressErrorHandler(server));\n *\n * Captures the error with request context, then forwards to the next\n * error handler — Crossdeck observes, the framework still produces the\n * normal 500 response.\n *\n * In Express 5 (async handlers natively forward errors), this middleware\n * sees errors from any handler. In Express 4, customers must wrap async\n * route handlers in a `next(err)` adapter — that's not a Crossdeck\n * limitation; it's how Express 4 works.\n */\nexport function crossdeckExpressErrorHandler(\n server: CrossdeckServer,\n options: CrossdeckExpressOptions = {},\n) {\n const attachContext = options.captureErrorsWithRequestContext !== false;\n\n return function crossdeckExpressErrorMiddleware(\n err: unknown,\n req: ExpressRequestLike,\n _res: ExpressResponseLike,\n next: ExpressNext,\n ): void {\n try {\n if (attachContext) {\n server.captureError(err, {\n context: {\n request: {\n url: req.originalUrl ?? req.url,\n method: req.method,\n route: extractRoutePattern(req),\n },\n },\n });\n } else {\n server.captureError(err);\n }\n } catch {\n // SDK observation must not block the framework's error pipeline.\n }\n next(err);\n };\n}\n\n// ---------- helpers (exported for testing) ----------\n\nexport function shouldSkipRequest(\n req: ExpressRequestLike,\n skipPaths: Array<string | RegExp>,\n): boolean {\n const candidates = [req.path, req.url].filter((s): s is string => typeof s === \"string\");\n for (const candidate of candidates) {\n for (const pattern of skipPaths) {\n if (typeof pattern === \"string\" && candidate.startsWith(pattern)) return true;\n if (pattern instanceof RegExp && pattern.test(candidate)) return true;\n }\n }\n return false;\n}\n\nfunction readHeader(req: ExpressRequestLike, name: string): string | undefined {\n if (!req.headers) return undefined;\n const v = req.headers[name];\n if (typeof v === \"string\") return v;\n if (Array.isArray(v)) return v[0];\n return undefined;\n}\n\nexport function extractRoutePattern(req: ExpressRequestLike): string {\n const routePath = req.route?.path;\n if (typeof routePath === \"string\") return routePath;\n if (routePath instanceof RegExp) return routePath.source;\n if (Array.isArray(routePath)) {\n return routePath\n .map((p) => (typeof p === \"string\" ? p : p instanceof RegExp ? p.source : \"\"))\n .filter(Boolean)\n .join(\"|\");\n }\n // Fallback for 404s + middleware-only requests where `req.route` is\n // undefined. Use `req.path` (the URL path without query string)\n // when present, else `req.url` as a last resort.\n return req.path ?? req.url ?? \"<unknown>\";\n}\n","/**\n * AWS Lambda handler wrapper — emits `function.invoked` /\n * `function.completed` / `function.failed` with Lambda lifecycle\n * metadata, and (crucially) `await server.flush()` BEFORE the handler\n * returns.\n *\n * Why flush-before-return is non-optional on Lambda: the runtime\n * freezes the process between invocations. Any event queued but not\n * sent over the wire vanishes — silently — when the function returns.\n * `flush-on-exit` doesn't fire because the process isn't exiting;\n * it's hibernating. Without the wrapper's explicit flush, you'd lose\n * the very telemetry you installed the SDK for.\n *\n * import { wrapLambdaHandler } from \"@cross-deck/node/auto-events\";\n *\n * export const handler = wrapLambdaHandler(server, async (event, ctx) => {\n * // your handler\n * });\n *\n * The wrapper preserves the handler's TypeScript signature via\n * generic parameters so the wrapped handler is type-equivalent to\n * the original.\n *\n * Cold-start detection is per-module-instance: the first invocation\n * gets `coldStart: true`, subsequent invocations of the SAME warm\n * container get `coldStart: false`. AWS spawns multiple containers\n * for concurrent invocations — each container's first invocation is\n * a cold start, so this is a per-container signal, not per-account.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\n\n/**\n * Minimal shape of the AWS Lambda invocation context. We don't pull\n * `@types/aws-lambda` as a dependency — that would force every\n * non-Lambda caller to install Lambda types just to import the SDK.\n * The fields we read are the stable subset every Lambda runtime\n * provides.\n */\nexport interface LambdaContextLike {\n awsRequestId?: string;\n functionName?: string;\n functionVersion?: string;\n invokedFunctionArn?: string;\n memoryLimitInMB?: number | string;\n logGroupName?: string;\n logStreamName?: string;\n /** Time remaining in the invocation, in ms. Useful for context. */\n getRemainingTimeInMillis?: () => number;\n}\n\nexport type LambdaHandlerLike<TEvent, TResult> = (\n event: TEvent,\n context: LambdaContextLike,\n) => Promise<TResult> | TResult;\n\nexport interface WrapLambdaOptions {\n /**\n * Override the per-container cold-start flag. Module-level\n * detection is sufficient for production; tests use this to\n * deterministically reset cold-start across runs.\n */\n resetColdStart?: boolean;\n /**\n * Optional identity extractor — read auth context from `event`\n * (e.g. `event.requestContext?.authorizer?.principalId` on an API\n * Gateway invocation) and attach to the emitted events.\n */\n getIdentity?: (event: unknown, context: LambdaContextLike) => {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n } | null | undefined;\n}\n\nlet containerColdStart = true;\n\n/**\n * Wrap a Lambda handler. Returns a handler with the same signature.\n *\n * Lifecycle emitted:\n * - `function.invoked` on entry — requestId, functionName, coldStart\n * - `function.completed` on success — durationMs, memoryUsedMb, statusCode\n * - `function.failed` on throw — errorType, errorMessage, durationMs\n *\n * Failures also call `server.captureError(err)` so the error pipeline\n * sees it with `error.handled` shape (frames + fingerprint +\n * breadcrumbs). The thrown error is re-thrown after capture so Lambda\n * itself still sees the failure and reports it to CloudWatch.\n *\n * `await server.flush()` runs in the `finally` block of every\n * invocation — bounded best-effort, so a transient backend outage\n * doesn't keep the function alive past the platform's SIGKILL.\n */\nexport function wrapLambdaHandler<TEvent, TResult>(\n server: CrossdeckServer,\n handler: LambdaHandlerLike<TEvent, TResult>,\n options: WrapLambdaOptions = {},\n): LambdaHandlerLike<TEvent, TResult> {\n if (options.resetColdStart === true) containerColdStart = true;\n\n return async function wrappedLambdaHandler(event, context): Promise<TResult> {\n const start = Date.now();\n const coldStart = containerColdStart;\n containerColdStart = false;\n const identity = safeExtractIdentity(options.getIdentity, event, context);\n\n server.track({\n name: \"function.invoked\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n functionVersion: context.functionVersion,\n coldStart,\n memoryLimitMb: numericOrUndefined(context.memoryLimitInMB),\n remainingMs: safeRemainingMs(context),\n },\n });\n\n try {\n const result = await handler(event, context);\n const completedProps: Record<string, unknown> = {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n durationMs: Date.now() - start,\n memoryUsedMb: rssMb(),\n };\n // API Gateway / Function URL responses are\n // `{ statusCode, body, headers? }`. When the handler returns\n // that shape, surface statusCode + body size on the completed\n // event so the dashboard can pivot by HTTP outcome. Non-HTTP\n // handlers (queue / cron) return arbitrary shapes; we silently\n // skip those keys.\n if (isHttpStyleResponse(result)) {\n completedProps.statusCode = result.statusCode;\n if (typeof result.body === \"string\") {\n completedProps.responseBytes = Buffer.byteLength(result.body, \"utf8\");\n }\n }\n server.track({\n name: \"function.completed\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: completedProps,\n });\n return result;\n } catch (err) {\n try {\n server.captureError(err, {\n context: {\n lambda: {\n requestId: context.awsRequestId,\n functionName: context.functionName,\n functionVersion: context.functionVersion,\n },\n },\n });\n } catch {\n // self-protection — error capture must never block re-throw\n }\n try {\n server.track({\n name: \"function.failed\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n errorType: err instanceof Error ? err.name : null,\n errorMessage: err instanceof Error ? err.message : String(err),\n durationMs: Date.now() - start,\n },\n });\n } catch {\n // swallow — same self-protection\n }\n throw err;\n } finally {\n // CRITICAL — Lambda freezes the process between invocations.\n // Without this, queued events vanish silently the moment the\n // handler returns. `flush()` is best-effort + bounded by the\n // queue's own timeout policy.\n try {\n await server.flush();\n } catch {\n // Flush failure is observable via diagnostics.events.lastError.\n }\n }\n };\n}\n\nfunction safeExtractIdentity(\n extractor: WrapLambdaOptions[\"getIdentity\"] | undefined,\n event: unknown,\n context: LambdaContextLike,\n): ReturnType<NonNullable<WrapLambdaOptions[\"getIdentity\"]>> {\n if (!extractor) return undefined;\n try {\n return extractor(event, context);\n } catch {\n return undefined;\n }\n}\n\nfunction safeRemainingMs(context: LambdaContextLike): number | undefined {\n try {\n return typeof context.getRemainingTimeInMillis === \"function\"\n ? context.getRemainingTimeInMillis()\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction numericOrUndefined(value: number | string | undefined): number | undefined {\n if (typeof value === \"number\") return value;\n if (typeof value === \"string\") {\n const n = Number(value);\n return Number.isFinite(n) ? n : undefined;\n }\n return undefined;\n}\n\nfunction rssMb(): number {\n try {\n return Math.round(process.memoryUsage().rss / 1024 / 1024);\n } catch {\n return 0;\n }\n}\n\n/**\n * Duck-type detection for API Gateway / Function URL / ALB-style\n * Lambda responses. Real Lambda handlers can return ANY shape — only\n * HTTP-style returns expose statusCode + body. We don't import\n * `@types/aws-lambda` to avoid the dep cost; this duck-type is good\n * enough.\n */\nfunction isHttpStyleResponse(\n value: unknown,\n): value is { statusCode: number; body?: string; headers?: Record<string, string> } {\n return Boolean(\n value &&\n typeof value === \"object\" &&\n typeof (value as { statusCode?: unknown }).statusCode === \"number\",\n );\n}\n","/**\n * Firebase Cloud Functions wrapper — generic across v1 + v2, also\n * usable for Google Cloud Run Functions and Cloud Run services\n * (anything that exposes a Node handler and freezes / tears down\n * between invocations).\n *\n * Why generic: Firebase has many handler signatures across v1 and v2:\n * v1 https.onRequest: (req, res) => void\n * v1 https.onCall: (data, context) => Promise<result>\n * v1 firestore.onWrite: (snapshot, context) => Promise<void>\n * v1 pubsub.onPublish: (message, context) => Promise<void>\n * v2 onRequest: (req, res) => Promise<void>\n * v2 onCall: (request) => Promise<result>\n * v2 onDocumentWritten: (event) => Promise<void>\n *\n * Rather than ship one wrapper per signature (which would force a\n * dependency on `firebase-functions` types and break when Google\n * adds new triggers), this wrapper is **shape-preserving** — it\n * accepts ANY function and returns one with the same signature.\n * Lifecycle telemetry is emitted around the call; metadata extraction\n * is plug-in via `getMetadata`.\n *\n * import { wrapFunction } from \"@cross-deck/node/auto-events\";\n * import { onRequest } from \"firebase-functions/v2/https\";\n *\n * export const myFunction = onRequest(wrapFunction(server, async (req, res) => {\n * // your handler\n * }));\n *\n * Cold-start detection: same per-container logic as Lambda. The\n * first invocation of a fresh container is a cold start; subsequent\n * invocations of the same warm container are not.\n *\n * Flush-before-return: same critical contract as Lambda. Firebase\n * tears down idle containers; queued events vanish if the SDK doesn't\n * flush before the handler returns.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\n\nexport interface WrapFunctionOptions {\n /**\n * Override the per-container cold-start flag. Module-level\n * detection is sufficient for production; tests use this to\n * deterministically reset cold-start across runs.\n */\n resetColdStart?: boolean;\n /**\n * Optional metadata extractor — read trigger-specific fields off\n * the handler arguments and attach them to the emitted events.\n * Default: no extra metadata.\n *\n * Return `{ identity, properties }` so identity hints route onto\n * the event envelope (for dashboard pivot) and properties merge\n * into the event's `properties` bag:\n *\n * wrapFunction(server, handler, {\n * getMetadata: (args) => ({\n * identity: { developerUserId: args[0].auth?.uid },\n * properties: { docPath: args[0].ref?.path, region: \"us-central1\" },\n * }),\n * })\n */\n getMetadata?: (args: unknown[]) => WrapFunctionMetadata | null | undefined;\n /**\n * Label for the `runtime` event property. Defaults to\n * `\"firebase-functions\"`. Override to distinguish triggers in\n * dashboards (e.g. `\"firebase-https\"` vs `\"firebase-firestore\"`).\n */\n runtime?: string;\n}\n\nexport interface WrapFunctionMetadata {\n /** Identity hint attached to the event envelope (developerUserId / anonymousId / crossdeckCustomerId). */\n identity?: {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n };\n /** Additional properties merged into emitted event properties. */\n properties?: Record<string, unknown>;\n}\n\nlet containerColdStart = true;\n\n/**\n * Shape-preserving wrap for a Firebase / Cloud Run handler. Returns\n * a function with the SAME signature as the input.\n *\n * Lifecycle emitted:\n * - `function.invoked` on entry — runtime, coldStart, ...metadata\n * - `function.completed` on success — durationMs, memoryUsedMb\n * - `function.failed` on throw — errorType, errorMessage, durationMs\n *\n * Failures also call `server.captureError(err)`. Errors are re-thrown\n * so Firebase still sees the failure and reports it to Cloud Logging.\n *\n * `await server.flush()` runs in `finally` — same as Lambda. Firebase\n * containers freeze / tear down between invocations.\n */\nexport function wrapFunction<TArgs extends unknown[], TResult>(\n server: CrossdeckServer,\n handler: (...args: TArgs) => Promise<TResult> | TResult,\n options: WrapFunctionOptions = {},\n): (...args: TArgs) => Promise<TResult> {\n if (options.resetColdStart === true) containerColdStart = true;\n const runtime = options.runtime ?? \"firebase-functions\";\n\n return async function wrappedFirebaseHandler(...args: TArgs): Promise<TResult> {\n const start = Date.now();\n const coldStart = containerColdStart;\n containerColdStart = false;\n const metadata = safeExtractMetadata(options.getMetadata, args);\n const identity = metadata?.identity ?? {};\n const extraProps = metadata?.properties ?? {};\n\n server.track({\n name: \"function.invoked\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n coldStart,\n ...extraProps,\n },\n });\n\n try {\n const result = await handler(...args);\n server.track({\n name: \"function.completed\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n durationMs: Date.now() - start,\n memoryUsedMb: rssMb(),\n ...extraProps,\n },\n });\n return result;\n } catch (err) {\n try {\n server.captureError(err, {\n context: { firebase: extraProps },\n });\n } catch {\n // self-protection — error capture must never block re-throw\n }\n try {\n server.track({\n name: \"function.failed\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n errorType: err instanceof Error ? err.name : null,\n errorMessage: err instanceof Error ? err.message : String(err),\n durationMs: Date.now() - start,\n ...extraProps,\n },\n });\n } catch {\n // swallow — same self-protection\n }\n throw err;\n } finally {\n try {\n await server.flush();\n } catch {\n // Flush failure is observable via diagnostics.events.lastError.\n }\n }\n };\n}\n\nfunction safeExtractMetadata(\n extractor: WrapFunctionOptions[\"getMetadata\"] | undefined,\n args: unknown[],\n): WrapFunctionMetadata | undefined {\n if (!extractor) return undefined;\n try {\n const out = extractor(args);\n return out ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction rssMb(): number {\n try {\n return Math.round(process.memoryUsage().rss / 1024 / 1024);\n } catch {\n return 0;\n }\n}\n"],"mappings":"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","names":["containerColdStart","rssMb"]}
|
|
1
|
+
{"version":3,"sources":["../../src/auto-events/index.ts","../../src/read-cost-bridge.ts","../../src/auto-events/express.ts","../../src/auto-events/lambda.ts","../../src/auto-events/firebase.ts"],"sourcesContent":["/**\n * @cross-deck/node/auto-events — framework adapters barrel.\n *\n * Each adapter is a thin wrapper around the core `CrossdeckServer`\n * API. They don't pull their framework's runtime types as a hard\n * dependency — the imports are shape-only — so a customer using\n * Express but not Firebase doesn't pay for `firebase-functions`\n * types, and vice versa.\n *\n * The three adapters approved for v1.0.0 (per the SDK_TRUTH.md\n * capability matrix Q1 decision):\n * - Express + ExpressErrorHandler — `request.handled` + route-error capture\n * - Lambda — `function.invoked` / `completed` / `failed` + flush-before-return\n * - Firebase / Cloud Run — generic `wrapFunction` for any handler shape\n *\n * Fastify is deferred to v0.3.0 (Q1 decision). Cloudflare Workers and\n * Vercel Edge are deferred to v0.4+ (no `process.on(...)` lifecycle\n * in Workers — flush-on-exit needs a runtime-specific pattern).\n */\n\nexport {\n crossdeckExpress,\n crossdeckExpressErrorHandler,\n shouldSkipRequest,\n extractRoutePattern,\n} from \"./express\";\nexport type {\n CrossdeckExpressOptions,\n ExpressNext,\n ExpressRequestLike,\n ExpressResponseLike,\n} from \"./express\";\n\nexport { wrapLambdaHandler } from \"./lambda\";\nexport type {\n LambdaContextLike,\n LambdaHandlerLike,\n WrapLambdaOptions,\n} from \"./lambda\";\n\nexport { wrapFunction } from \"./firebase\";\nexport type { WrapFunctionOptions, WrapFunctionMetadata } from \"./firebase\";\n","/**\n * Read-cost bridge — the one-way seam from the Crossdeck SDK to the Buckets OSS\n * collector (`@cross-deck/buckets`), without either package depending on the other.\n *\n * Buckets, at `init()`, registers a setter on a well-known global key. This module\n * looks that setter up and calls it. If Buckets isn't installed, the global is\n * absent and every call here is a silent no-op — the SDK never requires Buckets,\n * and Buckets never requires the SDK.\n *\n * What it carries is the cross-match input: WHO (the identified user behind a\n * request) and WHAT (the route / operation). Buckets stamps those onto every\n * database read that happens inside the request's async context, so a heavy read\n * attributes to \"this user's this operation\" instead of an anonymous collection.\n *\n * The global key and context shape MUST match `@cross-deck/buckets`'\n * `actor-bridge.ts` (`BUCKETS_BRIDGE_KEY`, `RequestContext`). They are a wire\n * contract between two independently-published packages — keep them in lockstep.\n */\n\n/** Must equal `BUCKETS_BRIDGE_KEY` in @cross-deck/buckets. */\nconst BUCKETS_BRIDGE_KEY = \"__crossdeckBucketsBridge__\";\n\n/** The cross-match context. Only the provided fields are applied. */\nexport interface ReadCostContext {\n /** WHO — the identified user behind this request (the developer's own id). */\n actor?: string;\n /** WHAT — the operation/feature that spent the reads. */\n feature?: string;\n /** WHAT (fallback) — the matched route pattern, e.g. `/users/:id`. */\n route?: string;\n}\n\ntype BridgeSetter = (ctx: ReadCostContext) => void;\n\n/**\n * Push the cross-match context into Buckets for the current request's async\n * context. No-op (and never throws) when the Buckets collector isn't installed.\n */\nexport function bridgeReadCost(ctx: ReadCostContext): void {\n try {\n const setter = (globalThis as Record<string, unknown>)[BUCKETS_BRIDGE_KEY] as\n | BridgeSetter\n | undefined;\n if (typeof setter === \"function\") setter(ctx);\n } catch {\n /* metering is best-effort — never disturb the host application */\n }\n}\n","/**\n * Express auto-events — `request.handled` middleware + uncaught-route\n * error capture.\n *\n * Two middleware factories, registered separately because Express\n * differentiates them by arity:\n *\n * import { crossdeckExpress, crossdeckExpressErrorHandler } from\n * \"@cross-deck/node/auto-events\";\n *\n * app.use(crossdeckExpress(server)); // request middleware\n * app.use(routes); // your routes\n * app.use(crossdeckExpressErrorHandler(server)); // LAST — error middleware\n *\n * `crossdeckExpress` emits `request.handled` on response 'finish'\n * with the matched route pattern (not the full URL — high-cardinality\n * URL paths kill dashboards), method, statusCode, and durationMs.\n *\n * `crossdeckExpressErrorHandler` catches errors thrown in route\n * handlers (sync OR async — Express 5 supports async handlers\n * natively; Express 4 needs the caller to forward via `next(err)`).\n * The error is shipped with request context (url, method, matched\n * route) attached so the dashboard can group by route.\n *\n * Compatible with both Express 4 and Express 5. The middleware\n * signatures are stable across both versions.\n *\n * No `import` from `express`. The adapter speaks shape-only against\n * Express's request / response objects — customers don't pay a\n * forced dependency on Express just to install the Crossdeck SDK.\n * If `express` is missing at install, this module still compiles.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\nimport { bridgeReadCost } from \"../read-cost-bridge\";\n\n/**\n * Shape of an Express request object — enough fields for the\n * middleware to do its job without depending on Express's types.\n */\nexport interface ExpressRequestLike {\n method: string;\n url: string;\n path?: string;\n route?: { path?: string | RegExp | Array<string | RegExp> };\n originalUrl?: string;\n headers?: Record<string, string | string[] | undefined>;\n}\n\n/** Shape of an Express response object. */\nexport interface ExpressResponseLike {\n statusCode: number;\n once(event: \"finish\" | \"close\", listener: () => void): unknown;\n /**\n * Optional — Express's response exposes this for reading headers\n * the framework / middleware chain set. Used by the middleware to\n * surface `responseBytes` on the `request.handled` event. If your\n * adapter doesn't have it, the field is simply omitted.\n */\n getHeader?(name: string): string | string[] | number | undefined;\n}\n\nexport type ExpressNext = (err?: unknown) => void;\n\nexport interface CrossdeckExpressOptions {\n /**\n * Routes to skip. Tested against `req.route?.path` if available, else\n * `req.path` / `req.url`. Defaults to a single self-skip for\n * `/crossdeck/*` so the SDK doesn't emit telemetry about its own\n * health endpoints.\n */\n skipPaths?: Array<string | RegExp>;\n /**\n * Optional identity extractor — runs once per request. Whatever it\n * returns is attached to the `request.handled` event so the\n * dashboard can pivot by user. Typical implementation: read\n * `req.user.id` populated by your auth middleware.\n *\n * crossdeckExpress(server, {\n * getIdentity: (req) => ({ developerUserId: req.user?.id }),\n * })\n */\n getIdentity?: (req: ExpressRequestLike) => {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n } | null | undefined;\n /**\n * Attach `{ url, method, route }` as `context.request` on captured\n * errors. Default `true`. Set `false` if you have a separate\n * mechanism for capturing request context (Pino bindings, etc).\n */\n captureErrorsWithRequestContext?: boolean;\n}\n\nconst DEFAULT_SKIP_PATHS: Array<string | RegExp> = [/^\\/crossdeck($|\\/)/];\n\n/**\n * Express middleware that emits `request.handled` per request.\n * Register BEFORE your routes:\n *\n * app.use(crossdeckExpress(server));\n *\n * Behaviour:\n * - Listens on `res.once('finish')` so we capture the FINAL\n * statusCode after any post-route middleware (compression, etc).\n * Also listens on `res.once('close')` to cover client-aborted\n * requests where 'finish' never fires.\n * - Idempotent per request: dispatches once regardless of which\n * terminal event fires first.\n * - `route` property is the matched route PATTERN (`/users/:id`),\n * not the full URL — keeps dashboard cardinality manageable. Falls\n * back to `req.path` when no route matched (404s).\n * - Errors thrown by `getIdentity` are swallowed and the event still\n * ships without identity — telemetry must NEVER break the request\n * pipeline.\n */\nexport function crossdeckExpress(\n server: CrossdeckServer,\n options: CrossdeckExpressOptions = {},\n) {\n const skipPaths = options.skipPaths ?? DEFAULT_SKIP_PATHS;\n\n return function crossdeckExpressMiddleware(\n req: ExpressRequestLike,\n res: ExpressResponseLike,\n next: ExpressNext,\n ): void {\n if (shouldSkipRequest(req, skipPaths)) {\n next();\n return;\n }\n\n const start = Date.now();\n let dispatched = false;\n\n // Read-cost cross-match (the moat): stamp WHO for this request *before* the\n // route handler runs, so every database read it issues attributes to the\n // identified user in Buckets. We set the actor here at entry — the route\n // pattern isn't resolved until Express matches downstream, so WHAT is named\n // by the handler's own `bucket()` tags / SDK feature capture. No-op unless\n // the Buckets collector is installed; never throws into the request.\n try {\n let actor: string | undefined;\n try {\n actor = options.getIdentity?.(req)?.developerUserId;\n } catch {\n // identity extraction must never break the request pipeline\n }\n if (actor) bridgeReadCost({ actor });\n } catch {\n // the bridge is best-effort — a missing collector is a silent no-op\n }\n\n const emit = (): void => {\n if (dispatched) return;\n dispatched = true;\n let identity: ReturnType<NonNullable<CrossdeckExpressOptions[\"getIdentity\"]>> = undefined;\n try {\n identity = options.getIdentity?.(req);\n } catch {\n // identity extraction must never break the request pipeline\n }\n try {\n const props: Record<string, unknown> = {\n route: extractRoutePattern(req),\n method: req.method,\n statusCode: res.statusCode,\n durationMs: Date.now() - start,\n };\n // Defensive: every header read is in a try/swallow because a\n // misbehaving framework middleware may have mutated the\n // response in ways that throw on access.\n try {\n const ua = readHeader(req, \"user-agent\");\n if (ua) props.userAgent = ua;\n } catch {\n // skip\n }\n try {\n const cl = typeof res.getHeader === \"function\" ? res.getHeader(\"content-length\") : undefined;\n if (typeof cl === \"number\") {\n props.responseBytes = cl;\n } else if (typeof cl === \"string\") {\n const parsed = Number(cl);\n if (Number.isFinite(parsed)) props.responseBytes = parsed;\n }\n } catch {\n // skip\n }\n server.track({\n name: \"request.handled\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: props,\n });\n } catch {\n // SDK telemetry must never throw out of the response pipeline.\n }\n };\n\n res.once(\"finish\", emit);\n res.once(\"close\", emit);\n\n next();\n };\n}\n\n/**\n * Express error middleware (4-arg signature). Register LAST, after\n * all routes + after the request middleware:\n *\n * app.use(crossdeckExpressErrorHandler(server));\n *\n * Captures the error with request context, then forwards to the next\n * error handler — Crossdeck observes, the framework still produces the\n * normal 500 response.\n *\n * In Express 5 (async handlers natively forward errors), this middleware\n * sees errors from any handler. In Express 4, customers must wrap async\n * route handlers in a `next(err)` adapter — that's not a Crossdeck\n * limitation; it's how Express 4 works.\n */\nexport function crossdeckExpressErrorHandler(\n server: CrossdeckServer,\n options: CrossdeckExpressOptions = {},\n) {\n const attachContext = options.captureErrorsWithRequestContext !== false;\n\n return function crossdeckExpressErrorMiddleware(\n err: unknown,\n req: ExpressRequestLike,\n _res: ExpressResponseLike,\n next: ExpressNext,\n ): void {\n try {\n if (attachContext) {\n server.captureError(err, {\n context: {\n request: {\n url: req.originalUrl ?? req.url,\n method: req.method,\n route: extractRoutePattern(req),\n },\n },\n });\n } else {\n server.captureError(err);\n }\n } catch {\n // SDK observation must not block the framework's error pipeline.\n }\n next(err);\n };\n}\n\n// ---------- helpers (exported for testing) ----------\n\nexport function shouldSkipRequest(\n req: ExpressRequestLike,\n skipPaths: Array<string | RegExp>,\n): boolean {\n const candidates = [req.path, req.url].filter((s): s is string => typeof s === \"string\");\n for (const candidate of candidates) {\n for (const pattern of skipPaths) {\n if (typeof pattern === \"string\" && candidate.startsWith(pattern)) return true;\n if (pattern instanceof RegExp && pattern.test(candidate)) return true;\n }\n }\n return false;\n}\n\nfunction readHeader(req: ExpressRequestLike, name: string): string | undefined {\n if (!req.headers) return undefined;\n const v = req.headers[name];\n if (typeof v === \"string\") return v;\n if (Array.isArray(v)) return v[0];\n return undefined;\n}\n\nexport function extractRoutePattern(req: ExpressRequestLike): string {\n const routePath = req.route?.path;\n if (typeof routePath === \"string\") return routePath;\n if (routePath instanceof RegExp) return routePath.source;\n if (Array.isArray(routePath)) {\n return routePath\n .map((p) => (typeof p === \"string\" ? p : p instanceof RegExp ? p.source : \"\"))\n .filter(Boolean)\n .join(\"|\");\n }\n // Fallback for 404s + middleware-only requests where `req.route` is\n // undefined. Use `req.path` (the URL path without query string)\n // when present, else `req.url` as a last resort.\n return req.path ?? req.url ?? \"<unknown>\";\n}\n","/**\n * AWS Lambda handler wrapper — emits `function.invoked` /\n * `function.completed` / `function.failed` with Lambda lifecycle\n * metadata, and (crucially) `await server.flush()` BEFORE the handler\n * returns.\n *\n * Why flush-before-return is non-optional on Lambda: the runtime\n * freezes the process between invocations. Any event queued but not\n * sent over the wire vanishes — silently — when the function returns.\n * `flush-on-exit` doesn't fire because the process isn't exiting;\n * it's hibernating. Without the wrapper's explicit flush, you'd lose\n * the very telemetry you installed the SDK for.\n *\n * import { wrapLambdaHandler } from \"@cross-deck/node/auto-events\";\n *\n * export const handler = wrapLambdaHandler(server, async (event, ctx) => {\n * // your handler\n * });\n *\n * The wrapper preserves the handler's TypeScript signature via\n * generic parameters so the wrapped handler is type-equivalent to\n * the original.\n *\n * Cold-start detection is per-module-instance: the first invocation\n * gets `coldStart: true`, subsequent invocations of the SAME warm\n * container get `coldStart: false`. AWS spawns multiple containers\n * for concurrent invocations — each container's first invocation is\n * a cold start, so this is a per-container signal, not per-account.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\nimport { bridgeReadCost } from \"../read-cost-bridge\";\n\n/**\n * Minimal shape of the AWS Lambda invocation context. We don't pull\n * `@types/aws-lambda` as a dependency — that would force every\n * non-Lambda caller to install Lambda types just to import the SDK.\n * The fields we read are the stable subset every Lambda runtime\n * provides.\n */\nexport interface LambdaContextLike {\n awsRequestId?: string;\n functionName?: string;\n functionVersion?: string;\n invokedFunctionArn?: string;\n memoryLimitInMB?: number | string;\n logGroupName?: string;\n logStreamName?: string;\n /** Time remaining in the invocation, in ms. Useful for context. */\n getRemainingTimeInMillis?: () => number;\n}\n\nexport type LambdaHandlerLike<TEvent, TResult> = (\n event: TEvent,\n context: LambdaContextLike,\n) => Promise<TResult> | TResult;\n\nexport interface WrapLambdaOptions {\n /**\n * Override the per-container cold-start flag. Module-level\n * detection is sufficient for production; tests use this to\n * deterministically reset cold-start across runs.\n */\n resetColdStart?: boolean;\n /**\n * Optional identity extractor — read auth context from `event`\n * (e.g. `event.requestContext?.authorizer?.principalId` on an API\n * Gateway invocation) and attach to the emitted events.\n */\n getIdentity?: (event: unknown, context: LambdaContextLike) => {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n } | null | undefined;\n}\n\nlet containerColdStart = true;\n\n/**\n * Wrap a Lambda handler. Returns a handler with the same signature.\n *\n * Lifecycle emitted:\n * - `function.invoked` on entry — requestId, functionName, coldStart\n * - `function.completed` on success — durationMs, memoryUsedMb, statusCode\n * - `function.failed` on throw — errorType, errorMessage, durationMs\n *\n * Failures also call `server.captureError(err)` so the error pipeline\n * sees it with `error.handled` shape (frames + fingerprint +\n * breadcrumbs). The thrown error is re-thrown after capture so Lambda\n * itself still sees the failure and reports it to CloudWatch.\n *\n * `await server.flush()` runs in the `finally` block of every\n * invocation — bounded best-effort, so a transient backend outage\n * doesn't keep the function alive past the platform's SIGKILL.\n */\nexport function wrapLambdaHandler<TEvent, TResult>(\n server: CrossdeckServer,\n handler: LambdaHandlerLike<TEvent, TResult>,\n options: WrapLambdaOptions = {},\n): LambdaHandlerLike<TEvent, TResult> {\n if (options.resetColdStart === true) containerColdStart = true;\n\n return async function wrappedLambdaHandler(event, context): Promise<TResult> {\n const start = Date.now();\n const coldStart = containerColdStart;\n containerColdStart = false;\n const identity = safeExtractIdentity(options.getIdentity, event, context);\n\n // Read-cost cross-match: stamp WHO + WHAT for this invocation before the\n // handler runs, so its database reads attribute to the user and the function.\n // The function name IS the operation on serverless — a natural WHAT. Each\n // invocation is its own async context, so this never leaks across requests.\n // No-op unless @cross-deck/buckets is installed; never throws.\n try {\n bridgeReadCost({ actor: identity?.developerUserId, feature: context.functionName });\n } catch {\n // best-effort — a missing collector is a silent no-op\n }\n\n server.track({\n name: \"function.invoked\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n functionVersion: context.functionVersion,\n coldStart,\n memoryLimitMb: numericOrUndefined(context.memoryLimitInMB),\n remainingMs: safeRemainingMs(context),\n },\n });\n\n try {\n const result = await handler(event, context);\n const completedProps: Record<string, unknown> = {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n durationMs: Date.now() - start,\n memoryUsedMb: rssMb(),\n };\n // API Gateway / Function URL responses are\n // `{ statusCode, body, headers? }`. When the handler returns\n // that shape, surface statusCode + body size on the completed\n // event so the dashboard can pivot by HTTP outcome. Non-HTTP\n // handlers (queue / cron) return arbitrary shapes; we silently\n // skip those keys.\n if (isHttpStyleResponse(result)) {\n completedProps.statusCode = result.statusCode;\n if (typeof result.body === \"string\") {\n completedProps.responseBytes = Buffer.byteLength(result.body, \"utf8\");\n }\n }\n server.track({\n name: \"function.completed\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: completedProps,\n });\n return result;\n } catch (err) {\n try {\n server.captureError(err, {\n context: {\n lambda: {\n requestId: context.awsRequestId,\n functionName: context.functionName,\n functionVersion: context.functionVersion,\n },\n },\n });\n } catch {\n // self-protection — error capture must never block re-throw\n }\n try {\n server.track({\n name: \"function.failed\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n errorType: err instanceof Error ? err.name : null,\n errorMessage: err instanceof Error ? err.message : String(err),\n durationMs: Date.now() - start,\n },\n });\n } catch {\n // swallow — same self-protection\n }\n throw err;\n } finally {\n // CRITICAL — Lambda freezes the process between invocations.\n // Without this, queued events vanish silently the moment the\n // handler returns. `flush()` is best-effort + bounded by the\n // queue's own timeout policy.\n try {\n await server.flush();\n } catch {\n // Flush failure is observable via diagnostics.events.lastError.\n }\n }\n };\n}\n\nfunction safeExtractIdentity(\n extractor: WrapLambdaOptions[\"getIdentity\"] | undefined,\n event: unknown,\n context: LambdaContextLike,\n): ReturnType<NonNullable<WrapLambdaOptions[\"getIdentity\"]>> {\n if (!extractor) return undefined;\n try {\n return extractor(event, context);\n } catch {\n return undefined;\n }\n}\n\nfunction safeRemainingMs(context: LambdaContextLike): number | undefined {\n try {\n return typeof context.getRemainingTimeInMillis === \"function\"\n ? context.getRemainingTimeInMillis()\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction numericOrUndefined(value: number | string | undefined): number | undefined {\n if (typeof value === \"number\") return value;\n if (typeof value === \"string\") {\n const n = Number(value);\n return Number.isFinite(n) ? n : undefined;\n }\n return undefined;\n}\n\nfunction rssMb(): number {\n try {\n return Math.round(process.memoryUsage().rss / 1024 / 1024);\n } catch {\n return 0;\n }\n}\n\n/**\n * Duck-type detection for API Gateway / Function URL / ALB-style\n * Lambda responses. Real Lambda handlers can return ANY shape — only\n * HTTP-style returns expose statusCode + body. We don't import\n * `@types/aws-lambda` to avoid the dep cost; this duck-type is good\n * enough.\n */\nfunction isHttpStyleResponse(\n value: unknown,\n): value is { statusCode: number; body?: string; headers?: Record<string, string> } {\n return Boolean(\n value &&\n typeof value === \"object\" &&\n typeof (value as { statusCode?: unknown }).statusCode === \"number\",\n );\n}\n","/**\n * Firebase Cloud Functions wrapper — generic across v1 + v2, also\n * usable for Google Cloud Run Functions and Cloud Run services\n * (anything that exposes a Node handler and freezes / tears down\n * between invocations).\n *\n * Why generic: Firebase has many handler signatures across v1 and v2:\n * v1 https.onRequest: (req, res) => void\n * v1 https.onCall: (data, context) => Promise<result>\n * v1 firestore.onWrite: (snapshot, context) => Promise<void>\n * v1 pubsub.onPublish: (message, context) => Promise<void>\n * v2 onRequest: (req, res) => Promise<void>\n * v2 onCall: (request) => Promise<result>\n * v2 onDocumentWritten: (event) => Promise<void>\n *\n * Rather than ship one wrapper per signature (which would force a\n * dependency on `firebase-functions` types and break when Google\n * adds new triggers), this wrapper is **shape-preserving** — it\n * accepts ANY function and returns one with the same signature.\n * Lifecycle telemetry is emitted around the call; metadata extraction\n * is plug-in via `getMetadata`.\n *\n * import { wrapFunction } from \"@cross-deck/node/auto-events\";\n * import { onRequest } from \"firebase-functions/v2/https\";\n *\n * export const myFunction = onRequest(wrapFunction(server, async (req, res) => {\n * // your handler\n * }));\n *\n * Cold-start detection: same per-container logic as Lambda. The\n * first invocation of a fresh container is a cold start; subsequent\n * invocations of the same warm container are not.\n *\n * Flush-before-return: same critical contract as Lambda. Firebase\n * tears down idle containers; queued events vanish if the SDK doesn't\n * flush before the handler returns.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\n\nexport interface WrapFunctionOptions {\n /**\n * Override the per-container cold-start flag. Module-level\n * detection is sufficient for production; tests use this to\n * deterministically reset cold-start across runs.\n */\n resetColdStart?: boolean;\n /**\n * Optional metadata extractor — read trigger-specific fields off\n * the handler arguments and attach them to the emitted events.\n * Default: no extra metadata.\n *\n * Return `{ identity, properties }` so identity hints route onto\n * the event envelope (for dashboard pivot) and properties merge\n * into the event's `properties` bag:\n *\n * wrapFunction(server, handler, {\n * getMetadata: (args) => ({\n * identity: { developerUserId: args[0].auth?.uid },\n * properties: { docPath: args[0].ref?.path, region: \"us-central1\" },\n * }),\n * })\n */\n getMetadata?: (args: unknown[]) => WrapFunctionMetadata | null | undefined;\n /**\n * Label for the `runtime` event property. Defaults to\n * `\"firebase-functions\"`. Override to distinguish triggers in\n * dashboards (e.g. `\"firebase-https\"` vs `\"firebase-firestore\"`).\n */\n runtime?: string;\n}\n\nexport interface WrapFunctionMetadata {\n /** Identity hint attached to the event envelope (developerUserId / anonymousId / crossdeckCustomerId). */\n identity?: {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n };\n /** Additional properties merged into emitted event properties. */\n properties?: Record<string, unknown>;\n}\n\nlet containerColdStart = true;\n\n/**\n * Shape-preserving wrap for a Firebase / Cloud Run handler. Returns\n * a function with the SAME signature as the input.\n *\n * Lifecycle emitted:\n * - `function.invoked` on entry — runtime, coldStart, ...metadata\n * - `function.completed` on success — durationMs, memoryUsedMb\n * - `function.failed` on throw — errorType, errorMessage, durationMs\n *\n * Failures also call `server.captureError(err)`. Errors are re-thrown\n * so Firebase still sees the failure and reports it to Cloud Logging.\n *\n * `await server.flush()` runs in `finally` — same as Lambda. Firebase\n * containers freeze / tear down between invocations.\n */\nexport function wrapFunction<TArgs extends unknown[], TResult>(\n server: CrossdeckServer,\n handler: (...args: TArgs) => Promise<TResult> | TResult,\n options: WrapFunctionOptions = {},\n): (...args: TArgs) => Promise<TResult> {\n if (options.resetColdStart === true) containerColdStart = true;\n const runtime = options.runtime ?? \"firebase-functions\";\n\n return async function wrappedFirebaseHandler(...args: TArgs): Promise<TResult> {\n const start = Date.now();\n const coldStart = containerColdStart;\n containerColdStart = false;\n const metadata = safeExtractMetadata(options.getMetadata, args);\n const identity = metadata?.identity ?? {};\n const extraProps = metadata?.properties ?? {};\n\n server.track({\n name: \"function.invoked\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n coldStart,\n ...extraProps,\n },\n });\n\n try {\n const result = await handler(...args);\n server.track({\n name: \"function.completed\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n durationMs: Date.now() - start,\n memoryUsedMb: rssMb(),\n ...extraProps,\n },\n });\n return result;\n } catch (err) {\n try {\n server.captureError(err, {\n context: { firebase: extraProps },\n });\n } catch {\n // self-protection — error capture must never block re-throw\n }\n try {\n server.track({\n name: \"function.failed\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n errorType: err instanceof Error ? err.name : null,\n errorMessage: err instanceof Error ? err.message : String(err),\n durationMs: Date.now() - start,\n ...extraProps,\n },\n });\n } catch {\n // swallow — same self-protection\n }\n throw err;\n } finally {\n try {\n await server.flush();\n } catch {\n // Flush failure is observable via diagnostics.events.lastError.\n }\n }\n };\n}\n\nfunction safeExtractMetadata(\n extractor: WrapFunctionOptions[\"getMetadata\"] | undefined,\n args: unknown[],\n): WrapFunctionMetadata | undefined {\n if (!extractor) return undefined;\n try {\n const out = extractor(args);\n return out ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction rssMb(): number {\n try {\n return Math.round(process.memoryUsage().rss / 1024 / 1024);\n } catch {\n return 0;\n }\n}\n"],"mappings":"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","names":["containerColdStart","rssMb"]}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
// src/read-cost-bridge.ts
|
|
2
|
+
var BUCKETS_BRIDGE_KEY = "__crossdeckBucketsBridge__";
|
|
3
|
+
function bridgeReadCost(ctx) {
|
|
4
|
+
try {
|
|
5
|
+
const setter = globalThis[BUCKETS_BRIDGE_KEY];
|
|
6
|
+
if (typeof setter === "function") setter(ctx);
|
|
7
|
+
} catch {
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
// src/auto-events/express.ts
|
|
2
12
|
var DEFAULT_SKIP_PATHS = [/^\/crossdeck($|\/)/];
|
|
3
13
|
function crossdeckExpress(server, options = {}) {
|
|
@@ -9,6 +19,15 @@ function crossdeckExpress(server, options = {}) {
|
|
|
9
19
|
}
|
|
10
20
|
const start = Date.now();
|
|
11
21
|
let dispatched = false;
|
|
22
|
+
try {
|
|
23
|
+
let actor;
|
|
24
|
+
try {
|
|
25
|
+
actor = options.getIdentity?.(req)?.developerUserId;
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
if (actor) bridgeReadCost({ actor });
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
12
31
|
const emit = () => {
|
|
13
32
|
if (dispatched) return;
|
|
14
33
|
dispatched = true;
|
|
@@ -112,6 +131,10 @@ function wrapLambdaHandler(server, handler, options = {}) {
|
|
|
112
131
|
const coldStart = containerColdStart;
|
|
113
132
|
containerColdStart = false;
|
|
114
133
|
const identity = safeExtractIdentity(options.getIdentity, event, context);
|
|
134
|
+
try {
|
|
135
|
+
bridgeReadCost({ actor: identity?.developerUserId, feature: context.functionName });
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
115
138
|
server.track({
|
|
116
139
|
name: "function.invoked",
|
|
117
140
|
developerUserId: identity?.developerUserId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auto-events/express.ts","../../src/auto-events/lambda.ts","../../src/auto-events/firebase.ts"],"sourcesContent":["/**\n * Express auto-events — `request.handled` middleware + uncaught-route\n * error capture.\n *\n * Two middleware factories, registered separately because Express\n * differentiates them by arity:\n *\n * import { crossdeckExpress, crossdeckExpressErrorHandler } from\n * \"@cross-deck/node/auto-events\";\n *\n * app.use(crossdeckExpress(server)); // request middleware\n * app.use(routes); // your routes\n * app.use(crossdeckExpressErrorHandler(server)); // LAST — error middleware\n *\n * `crossdeckExpress` emits `request.handled` on response 'finish'\n * with the matched route pattern (not the full URL — high-cardinality\n * URL paths kill dashboards), method, statusCode, and durationMs.\n *\n * `crossdeckExpressErrorHandler` catches errors thrown in route\n * handlers (sync OR async — Express 5 supports async handlers\n * natively; Express 4 needs the caller to forward via `next(err)`).\n * The error is shipped with request context (url, method, matched\n * route) attached so the dashboard can group by route.\n *\n * Compatible with both Express 4 and Express 5. The middleware\n * signatures are stable across both versions.\n *\n * No `import` from `express`. The adapter speaks shape-only against\n * Express's request / response objects — customers don't pay a\n * forced dependency on Express just to install the Crossdeck SDK.\n * If `express` is missing at install, this module still compiles.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\n\n/**\n * Shape of an Express request object — enough fields for the\n * middleware to do its job without depending on Express's types.\n */\nexport interface ExpressRequestLike {\n method: string;\n url: string;\n path?: string;\n route?: { path?: string | RegExp | Array<string | RegExp> };\n originalUrl?: string;\n headers?: Record<string, string | string[] | undefined>;\n}\n\n/** Shape of an Express response object. */\nexport interface ExpressResponseLike {\n statusCode: number;\n once(event: \"finish\" | \"close\", listener: () => void): unknown;\n /**\n * Optional — Express's response exposes this for reading headers\n * the framework / middleware chain set. Used by the middleware to\n * surface `responseBytes` on the `request.handled` event. If your\n * adapter doesn't have it, the field is simply omitted.\n */\n getHeader?(name: string): string | string[] | number | undefined;\n}\n\nexport type ExpressNext = (err?: unknown) => void;\n\nexport interface CrossdeckExpressOptions {\n /**\n * Routes to skip. Tested against `req.route?.path` if available, else\n * `req.path` / `req.url`. Defaults to a single self-skip for\n * `/crossdeck/*` so the SDK doesn't emit telemetry about its own\n * health endpoints.\n */\n skipPaths?: Array<string | RegExp>;\n /**\n * Optional identity extractor — runs once per request. Whatever it\n * returns is attached to the `request.handled` event so the\n * dashboard can pivot by user. Typical implementation: read\n * `req.user.id` populated by your auth middleware.\n *\n * crossdeckExpress(server, {\n * getIdentity: (req) => ({ developerUserId: req.user?.id }),\n * })\n */\n getIdentity?: (req: ExpressRequestLike) => {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n } | null | undefined;\n /**\n * Attach `{ url, method, route }` as `context.request` on captured\n * errors. Default `true`. Set `false` if you have a separate\n * mechanism for capturing request context (Pino bindings, etc).\n */\n captureErrorsWithRequestContext?: boolean;\n}\n\nconst DEFAULT_SKIP_PATHS: Array<string | RegExp> = [/^\\/crossdeck($|\\/)/];\n\n/**\n * Express middleware that emits `request.handled` per request.\n * Register BEFORE your routes:\n *\n * app.use(crossdeckExpress(server));\n *\n * Behaviour:\n * - Listens on `res.once('finish')` so we capture the FINAL\n * statusCode after any post-route middleware (compression, etc).\n * Also listens on `res.once('close')` to cover client-aborted\n * requests where 'finish' never fires.\n * - Idempotent per request: dispatches once regardless of which\n * terminal event fires first.\n * - `route` property is the matched route PATTERN (`/users/:id`),\n * not the full URL — keeps dashboard cardinality manageable. Falls\n * back to `req.path` when no route matched (404s).\n * - Errors thrown by `getIdentity` are swallowed and the event still\n * ships without identity — telemetry must NEVER break the request\n * pipeline.\n */\nexport function crossdeckExpress(\n server: CrossdeckServer,\n options: CrossdeckExpressOptions = {},\n) {\n const skipPaths = options.skipPaths ?? DEFAULT_SKIP_PATHS;\n\n return function crossdeckExpressMiddleware(\n req: ExpressRequestLike,\n res: ExpressResponseLike,\n next: ExpressNext,\n ): void {\n if (shouldSkipRequest(req, skipPaths)) {\n next();\n return;\n }\n\n const start = Date.now();\n let dispatched = false;\n\n const emit = (): void => {\n if (dispatched) return;\n dispatched = true;\n let identity: ReturnType<NonNullable<CrossdeckExpressOptions[\"getIdentity\"]>> = undefined;\n try {\n identity = options.getIdentity?.(req);\n } catch {\n // identity extraction must never break the request pipeline\n }\n try {\n const props: Record<string, unknown> = {\n route: extractRoutePattern(req),\n method: req.method,\n statusCode: res.statusCode,\n durationMs: Date.now() - start,\n };\n // Defensive: every header read is in a try/swallow because a\n // misbehaving framework middleware may have mutated the\n // response in ways that throw on access.\n try {\n const ua = readHeader(req, \"user-agent\");\n if (ua) props.userAgent = ua;\n } catch {\n // skip\n }\n try {\n const cl = typeof res.getHeader === \"function\" ? res.getHeader(\"content-length\") : undefined;\n if (typeof cl === \"number\") {\n props.responseBytes = cl;\n } else if (typeof cl === \"string\") {\n const parsed = Number(cl);\n if (Number.isFinite(parsed)) props.responseBytes = parsed;\n }\n } catch {\n // skip\n }\n server.track({\n name: \"request.handled\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: props,\n });\n } catch {\n // SDK telemetry must never throw out of the response pipeline.\n }\n };\n\n res.once(\"finish\", emit);\n res.once(\"close\", emit);\n\n next();\n };\n}\n\n/**\n * Express error middleware (4-arg signature). Register LAST, after\n * all routes + after the request middleware:\n *\n * app.use(crossdeckExpressErrorHandler(server));\n *\n * Captures the error with request context, then forwards to the next\n * error handler — Crossdeck observes, the framework still produces the\n * normal 500 response.\n *\n * In Express 5 (async handlers natively forward errors), this middleware\n * sees errors from any handler. In Express 4, customers must wrap async\n * route handlers in a `next(err)` adapter — that's not a Crossdeck\n * limitation; it's how Express 4 works.\n */\nexport function crossdeckExpressErrorHandler(\n server: CrossdeckServer,\n options: CrossdeckExpressOptions = {},\n) {\n const attachContext = options.captureErrorsWithRequestContext !== false;\n\n return function crossdeckExpressErrorMiddleware(\n err: unknown,\n req: ExpressRequestLike,\n _res: ExpressResponseLike,\n next: ExpressNext,\n ): void {\n try {\n if (attachContext) {\n server.captureError(err, {\n context: {\n request: {\n url: req.originalUrl ?? req.url,\n method: req.method,\n route: extractRoutePattern(req),\n },\n },\n });\n } else {\n server.captureError(err);\n }\n } catch {\n // SDK observation must not block the framework's error pipeline.\n }\n next(err);\n };\n}\n\n// ---------- helpers (exported for testing) ----------\n\nexport function shouldSkipRequest(\n req: ExpressRequestLike,\n skipPaths: Array<string | RegExp>,\n): boolean {\n const candidates = [req.path, req.url].filter((s): s is string => typeof s === \"string\");\n for (const candidate of candidates) {\n for (const pattern of skipPaths) {\n if (typeof pattern === \"string\" && candidate.startsWith(pattern)) return true;\n if (pattern instanceof RegExp && pattern.test(candidate)) return true;\n }\n }\n return false;\n}\n\nfunction readHeader(req: ExpressRequestLike, name: string): string | undefined {\n if (!req.headers) return undefined;\n const v = req.headers[name];\n if (typeof v === \"string\") return v;\n if (Array.isArray(v)) return v[0];\n return undefined;\n}\n\nexport function extractRoutePattern(req: ExpressRequestLike): string {\n const routePath = req.route?.path;\n if (typeof routePath === \"string\") return routePath;\n if (routePath instanceof RegExp) return routePath.source;\n if (Array.isArray(routePath)) {\n return routePath\n .map((p) => (typeof p === \"string\" ? p : p instanceof RegExp ? p.source : \"\"))\n .filter(Boolean)\n .join(\"|\");\n }\n // Fallback for 404s + middleware-only requests where `req.route` is\n // undefined. Use `req.path` (the URL path without query string)\n // when present, else `req.url` as a last resort.\n return req.path ?? req.url ?? \"<unknown>\";\n}\n","/**\n * AWS Lambda handler wrapper — emits `function.invoked` /\n * `function.completed` / `function.failed` with Lambda lifecycle\n * metadata, and (crucially) `await server.flush()` BEFORE the handler\n * returns.\n *\n * Why flush-before-return is non-optional on Lambda: the runtime\n * freezes the process between invocations. Any event queued but not\n * sent over the wire vanishes — silently — when the function returns.\n * `flush-on-exit` doesn't fire because the process isn't exiting;\n * it's hibernating. Without the wrapper's explicit flush, you'd lose\n * the very telemetry you installed the SDK for.\n *\n * import { wrapLambdaHandler } from \"@cross-deck/node/auto-events\";\n *\n * export const handler = wrapLambdaHandler(server, async (event, ctx) => {\n * // your handler\n * });\n *\n * The wrapper preserves the handler's TypeScript signature via\n * generic parameters so the wrapped handler is type-equivalent to\n * the original.\n *\n * Cold-start detection is per-module-instance: the first invocation\n * gets `coldStart: true`, subsequent invocations of the SAME warm\n * container get `coldStart: false`. AWS spawns multiple containers\n * for concurrent invocations — each container's first invocation is\n * a cold start, so this is a per-container signal, not per-account.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\n\n/**\n * Minimal shape of the AWS Lambda invocation context. We don't pull\n * `@types/aws-lambda` as a dependency — that would force every\n * non-Lambda caller to install Lambda types just to import the SDK.\n * The fields we read are the stable subset every Lambda runtime\n * provides.\n */\nexport interface LambdaContextLike {\n awsRequestId?: string;\n functionName?: string;\n functionVersion?: string;\n invokedFunctionArn?: string;\n memoryLimitInMB?: number | string;\n logGroupName?: string;\n logStreamName?: string;\n /** Time remaining in the invocation, in ms. Useful for context. */\n getRemainingTimeInMillis?: () => number;\n}\n\nexport type LambdaHandlerLike<TEvent, TResult> = (\n event: TEvent,\n context: LambdaContextLike,\n) => Promise<TResult> | TResult;\n\nexport interface WrapLambdaOptions {\n /**\n * Override the per-container cold-start flag. Module-level\n * detection is sufficient for production; tests use this to\n * deterministically reset cold-start across runs.\n */\n resetColdStart?: boolean;\n /**\n * Optional identity extractor — read auth context from `event`\n * (e.g. `event.requestContext?.authorizer?.principalId` on an API\n * Gateway invocation) and attach to the emitted events.\n */\n getIdentity?: (event: unknown, context: LambdaContextLike) => {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n } | null | undefined;\n}\n\nlet containerColdStart = true;\n\n/**\n * Wrap a Lambda handler. Returns a handler with the same signature.\n *\n * Lifecycle emitted:\n * - `function.invoked` on entry — requestId, functionName, coldStart\n * - `function.completed` on success — durationMs, memoryUsedMb, statusCode\n * - `function.failed` on throw — errorType, errorMessage, durationMs\n *\n * Failures also call `server.captureError(err)` so the error pipeline\n * sees it with `error.handled` shape (frames + fingerprint +\n * breadcrumbs). The thrown error is re-thrown after capture so Lambda\n * itself still sees the failure and reports it to CloudWatch.\n *\n * `await server.flush()` runs in the `finally` block of every\n * invocation — bounded best-effort, so a transient backend outage\n * doesn't keep the function alive past the platform's SIGKILL.\n */\nexport function wrapLambdaHandler<TEvent, TResult>(\n server: CrossdeckServer,\n handler: LambdaHandlerLike<TEvent, TResult>,\n options: WrapLambdaOptions = {},\n): LambdaHandlerLike<TEvent, TResult> {\n if (options.resetColdStart === true) containerColdStart = true;\n\n return async function wrappedLambdaHandler(event, context): Promise<TResult> {\n const start = Date.now();\n const coldStart = containerColdStart;\n containerColdStart = false;\n const identity = safeExtractIdentity(options.getIdentity, event, context);\n\n server.track({\n name: \"function.invoked\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n functionVersion: context.functionVersion,\n coldStart,\n memoryLimitMb: numericOrUndefined(context.memoryLimitInMB),\n remainingMs: safeRemainingMs(context),\n },\n });\n\n try {\n const result = await handler(event, context);\n const completedProps: Record<string, unknown> = {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n durationMs: Date.now() - start,\n memoryUsedMb: rssMb(),\n };\n // API Gateway / Function URL responses are\n // `{ statusCode, body, headers? }`. When the handler returns\n // that shape, surface statusCode + body size on the completed\n // event so the dashboard can pivot by HTTP outcome. Non-HTTP\n // handlers (queue / cron) return arbitrary shapes; we silently\n // skip those keys.\n if (isHttpStyleResponse(result)) {\n completedProps.statusCode = result.statusCode;\n if (typeof result.body === \"string\") {\n completedProps.responseBytes = Buffer.byteLength(result.body, \"utf8\");\n }\n }\n server.track({\n name: \"function.completed\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: completedProps,\n });\n return result;\n } catch (err) {\n try {\n server.captureError(err, {\n context: {\n lambda: {\n requestId: context.awsRequestId,\n functionName: context.functionName,\n functionVersion: context.functionVersion,\n },\n },\n });\n } catch {\n // self-protection — error capture must never block re-throw\n }\n try {\n server.track({\n name: \"function.failed\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n errorType: err instanceof Error ? err.name : null,\n errorMessage: err instanceof Error ? err.message : String(err),\n durationMs: Date.now() - start,\n },\n });\n } catch {\n // swallow — same self-protection\n }\n throw err;\n } finally {\n // CRITICAL — Lambda freezes the process between invocations.\n // Without this, queued events vanish silently the moment the\n // handler returns. `flush()` is best-effort + bounded by the\n // queue's own timeout policy.\n try {\n await server.flush();\n } catch {\n // Flush failure is observable via diagnostics.events.lastError.\n }\n }\n };\n}\n\nfunction safeExtractIdentity(\n extractor: WrapLambdaOptions[\"getIdentity\"] | undefined,\n event: unknown,\n context: LambdaContextLike,\n): ReturnType<NonNullable<WrapLambdaOptions[\"getIdentity\"]>> {\n if (!extractor) return undefined;\n try {\n return extractor(event, context);\n } catch {\n return undefined;\n }\n}\n\nfunction safeRemainingMs(context: LambdaContextLike): number | undefined {\n try {\n return typeof context.getRemainingTimeInMillis === \"function\"\n ? context.getRemainingTimeInMillis()\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction numericOrUndefined(value: number | string | undefined): number | undefined {\n if (typeof value === \"number\") return value;\n if (typeof value === \"string\") {\n const n = Number(value);\n return Number.isFinite(n) ? n : undefined;\n }\n return undefined;\n}\n\nfunction rssMb(): number {\n try {\n return Math.round(process.memoryUsage().rss / 1024 / 1024);\n } catch {\n return 0;\n }\n}\n\n/**\n * Duck-type detection for API Gateway / Function URL / ALB-style\n * Lambda responses. Real Lambda handlers can return ANY shape — only\n * HTTP-style returns expose statusCode + body. We don't import\n * `@types/aws-lambda` to avoid the dep cost; this duck-type is good\n * enough.\n */\nfunction isHttpStyleResponse(\n value: unknown,\n): value is { statusCode: number; body?: string; headers?: Record<string, string> } {\n return Boolean(\n value &&\n typeof value === \"object\" &&\n typeof (value as { statusCode?: unknown }).statusCode === \"number\",\n );\n}\n","/**\n * Firebase Cloud Functions wrapper — generic across v1 + v2, also\n * usable for Google Cloud Run Functions and Cloud Run services\n * (anything that exposes a Node handler and freezes / tears down\n * between invocations).\n *\n * Why generic: Firebase has many handler signatures across v1 and v2:\n * v1 https.onRequest: (req, res) => void\n * v1 https.onCall: (data, context) => Promise<result>\n * v1 firestore.onWrite: (snapshot, context) => Promise<void>\n * v1 pubsub.onPublish: (message, context) => Promise<void>\n * v2 onRequest: (req, res) => Promise<void>\n * v2 onCall: (request) => Promise<result>\n * v2 onDocumentWritten: (event) => Promise<void>\n *\n * Rather than ship one wrapper per signature (which would force a\n * dependency on `firebase-functions` types and break when Google\n * adds new triggers), this wrapper is **shape-preserving** — it\n * accepts ANY function and returns one with the same signature.\n * Lifecycle telemetry is emitted around the call; metadata extraction\n * is plug-in via `getMetadata`.\n *\n * import { wrapFunction } from \"@cross-deck/node/auto-events\";\n * import { onRequest } from \"firebase-functions/v2/https\";\n *\n * export const myFunction = onRequest(wrapFunction(server, async (req, res) => {\n * // your handler\n * }));\n *\n * Cold-start detection: same per-container logic as Lambda. The\n * first invocation of a fresh container is a cold start; subsequent\n * invocations of the same warm container are not.\n *\n * Flush-before-return: same critical contract as Lambda. Firebase\n * tears down idle containers; queued events vanish if the SDK doesn't\n * flush before the handler returns.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\n\nexport interface WrapFunctionOptions {\n /**\n * Override the per-container cold-start flag. Module-level\n * detection is sufficient for production; tests use this to\n * deterministically reset cold-start across runs.\n */\n resetColdStart?: boolean;\n /**\n * Optional metadata extractor — read trigger-specific fields off\n * the handler arguments and attach them to the emitted events.\n * Default: no extra metadata.\n *\n * Return `{ identity, properties }` so identity hints route onto\n * the event envelope (for dashboard pivot) and properties merge\n * into the event's `properties` bag:\n *\n * wrapFunction(server, handler, {\n * getMetadata: (args) => ({\n * identity: { developerUserId: args[0].auth?.uid },\n * properties: { docPath: args[0].ref?.path, region: \"us-central1\" },\n * }),\n * })\n */\n getMetadata?: (args: unknown[]) => WrapFunctionMetadata | null | undefined;\n /**\n * Label for the `runtime` event property. Defaults to\n * `\"firebase-functions\"`. Override to distinguish triggers in\n * dashboards (e.g. `\"firebase-https\"` vs `\"firebase-firestore\"`).\n */\n runtime?: string;\n}\n\nexport interface WrapFunctionMetadata {\n /** Identity hint attached to the event envelope (developerUserId / anonymousId / crossdeckCustomerId). */\n identity?: {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n };\n /** Additional properties merged into emitted event properties. */\n properties?: Record<string, unknown>;\n}\n\nlet containerColdStart = true;\n\n/**\n * Shape-preserving wrap for a Firebase / Cloud Run handler. Returns\n * a function with the SAME signature as the input.\n *\n * Lifecycle emitted:\n * - `function.invoked` on entry — runtime, coldStart, ...metadata\n * - `function.completed` on success — durationMs, memoryUsedMb\n * - `function.failed` on throw — errorType, errorMessage, durationMs\n *\n * Failures also call `server.captureError(err)`. Errors are re-thrown\n * so Firebase still sees the failure and reports it to Cloud Logging.\n *\n * `await server.flush()` runs in `finally` — same as Lambda. Firebase\n * containers freeze / tear down between invocations.\n */\nexport function wrapFunction<TArgs extends unknown[], TResult>(\n server: CrossdeckServer,\n handler: (...args: TArgs) => Promise<TResult> | TResult,\n options: WrapFunctionOptions = {},\n): (...args: TArgs) => Promise<TResult> {\n if (options.resetColdStart === true) containerColdStart = true;\n const runtime = options.runtime ?? \"firebase-functions\";\n\n return async function wrappedFirebaseHandler(...args: TArgs): Promise<TResult> {\n const start = Date.now();\n const coldStart = containerColdStart;\n containerColdStart = false;\n const metadata = safeExtractMetadata(options.getMetadata, args);\n const identity = metadata?.identity ?? {};\n const extraProps = metadata?.properties ?? {};\n\n server.track({\n name: \"function.invoked\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n coldStart,\n ...extraProps,\n },\n });\n\n try {\n const result = await handler(...args);\n server.track({\n name: \"function.completed\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n durationMs: Date.now() - start,\n memoryUsedMb: rssMb(),\n ...extraProps,\n },\n });\n return result;\n } catch (err) {\n try {\n server.captureError(err, {\n context: { firebase: extraProps },\n });\n } catch {\n // self-protection — error capture must never block re-throw\n }\n try {\n server.track({\n name: \"function.failed\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n errorType: err instanceof Error ? err.name : null,\n errorMessage: err instanceof Error ? err.message : String(err),\n durationMs: Date.now() - start,\n ...extraProps,\n },\n });\n } catch {\n // swallow — same self-protection\n }\n throw err;\n } finally {\n try {\n await server.flush();\n } catch {\n // Flush failure is observable via diagnostics.events.lastError.\n }\n }\n };\n}\n\nfunction safeExtractMetadata(\n extractor: WrapFunctionOptions[\"getMetadata\"] | undefined,\n args: unknown[],\n): WrapFunctionMetadata | undefined {\n if (!extractor) return undefined;\n try {\n const out = extractor(args);\n return out ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction rssMb(): number {\n try {\n return Math.round(process.memoryUsage().rss / 1024 / 1024);\n } catch {\n return 0;\n }\n}\n"],"mappings":"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","names":["containerColdStart","rssMb"]}
|
|
1
|
+
{"version":3,"sources":["../../src/read-cost-bridge.ts","../../src/auto-events/express.ts","../../src/auto-events/lambda.ts","../../src/auto-events/firebase.ts"],"sourcesContent":["/**\n * Read-cost bridge — the one-way seam from the Crossdeck SDK to the Buckets OSS\n * collector (`@cross-deck/buckets`), without either package depending on the other.\n *\n * Buckets, at `init()`, registers a setter on a well-known global key. This module\n * looks that setter up and calls it. If Buckets isn't installed, the global is\n * absent and every call here is a silent no-op — the SDK never requires Buckets,\n * and Buckets never requires the SDK.\n *\n * What it carries is the cross-match input: WHO (the identified user behind a\n * request) and WHAT (the route / operation). Buckets stamps those onto every\n * database read that happens inside the request's async context, so a heavy read\n * attributes to \"this user's this operation\" instead of an anonymous collection.\n *\n * The global key and context shape MUST match `@cross-deck/buckets`'\n * `actor-bridge.ts` (`BUCKETS_BRIDGE_KEY`, `RequestContext`). They are a wire\n * contract between two independently-published packages — keep them in lockstep.\n */\n\n/** Must equal `BUCKETS_BRIDGE_KEY` in @cross-deck/buckets. */\nconst BUCKETS_BRIDGE_KEY = \"__crossdeckBucketsBridge__\";\n\n/** The cross-match context. Only the provided fields are applied. */\nexport interface ReadCostContext {\n /** WHO — the identified user behind this request (the developer's own id). */\n actor?: string;\n /** WHAT — the operation/feature that spent the reads. */\n feature?: string;\n /** WHAT (fallback) — the matched route pattern, e.g. `/users/:id`. */\n route?: string;\n}\n\ntype BridgeSetter = (ctx: ReadCostContext) => void;\n\n/**\n * Push the cross-match context into Buckets for the current request's async\n * context. No-op (and never throws) when the Buckets collector isn't installed.\n */\nexport function bridgeReadCost(ctx: ReadCostContext): void {\n try {\n const setter = (globalThis as Record<string, unknown>)[BUCKETS_BRIDGE_KEY] as\n | BridgeSetter\n | undefined;\n if (typeof setter === \"function\") setter(ctx);\n } catch {\n /* metering is best-effort — never disturb the host application */\n }\n}\n","/**\n * Express auto-events — `request.handled` middleware + uncaught-route\n * error capture.\n *\n * Two middleware factories, registered separately because Express\n * differentiates them by arity:\n *\n * import { crossdeckExpress, crossdeckExpressErrorHandler } from\n * \"@cross-deck/node/auto-events\";\n *\n * app.use(crossdeckExpress(server)); // request middleware\n * app.use(routes); // your routes\n * app.use(crossdeckExpressErrorHandler(server)); // LAST — error middleware\n *\n * `crossdeckExpress` emits `request.handled` on response 'finish'\n * with the matched route pattern (not the full URL — high-cardinality\n * URL paths kill dashboards), method, statusCode, and durationMs.\n *\n * `crossdeckExpressErrorHandler` catches errors thrown in route\n * handlers (sync OR async — Express 5 supports async handlers\n * natively; Express 4 needs the caller to forward via `next(err)`).\n * The error is shipped with request context (url, method, matched\n * route) attached so the dashboard can group by route.\n *\n * Compatible with both Express 4 and Express 5. The middleware\n * signatures are stable across both versions.\n *\n * No `import` from `express`. The adapter speaks shape-only against\n * Express's request / response objects — customers don't pay a\n * forced dependency on Express just to install the Crossdeck SDK.\n * If `express` is missing at install, this module still compiles.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\nimport { bridgeReadCost } from \"../read-cost-bridge\";\n\n/**\n * Shape of an Express request object — enough fields for the\n * middleware to do its job without depending on Express's types.\n */\nexport interface ExpressRequestLike {\n method: string;\n url: string;\n path?: string;\n route?: { path?: string | RegExp | Array<string | RegExp> };\n originalUrl?: string;\n headers?: Record<string, string | string[] | undefined>;\n}\n\n/** Shape of an Express response object. */\nexport interface ExpressResponseLike {\n statusCode: number;\n once(event: \"finish\" | \"close\", listener: () => void): unknown;\n /**\n * Optional — Express's response exposes this for reading headers\n * the framework / middleware chain set. Used by the middleware to\n * surface `responseBytes` on the `request.handled` event. If your\n * adapter doesn't have it, the field is simply omitted.\n */\n getHeader?(name: string): string | string[] | number | undefined;\n}\n\nexport type ExpressNext = (err?: unknown) => void;\n\nexport interface CrossdeckExpressOptions {\n /**\n * Routes to skip. Tested against `req.route?.path` if available, else\n * `req.path` / `req.url`. Defaults to a single self-skip for\n * `/crossdeck/*` so the SDK doesn't emit telemetry about its own\n * health endpoints.\n */\n skipPaths?: Array<string | RegExp>;\n /**\n * Optional identity extractor — runs once per request. Whatever it\n * returns is attached to the `request.handled` event so the\n * dashboard can pivot by user. Typical implementation: read\n * `req.user.id` populated by your auth middleware.\n *\n * crossdeckExpress(server, {\n * getIdentity: (req) => ({ developerUserId: req.user?.id }),\n * })\n */\n getIdentity?: (req: ExpressRequestLike) => {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n } | null | undefined;\n /**\n * Attach `{ url, method, route }` as `context.request` on captured\n * errors. Default `true`. Set `false` if you have a separate\n * mechanism for capturing request context (Pino bindings, etc).\n */\n captureErrorsWithRequestContext?: boolean;\n}\n\nconst DEFAULT_SKIP_PATHS: Array<string | RegExp> = [/^\\/crossdeck($|\\/)/];\n\n/**\n * Express middleware that emits `request.handled` per request.\n * Register BEFORE your routes:\n *\n * app.use(crossdeckExpress(server));\n *\n * Behaviour:\n * - Listens on `res.once('finish')` so we capture the FINAL\n * statusCode after any post-route middleware (compression, etc).\n * Also listens on `res.once('close')` to cover client-aborted\n * requests where 'finish' never fires.\n * - Idempotent per request: dispatches once regardless of which\n * terminal event fires first.\n * - `route` property is the matched route PATTERN (`/users/:id`),\n * not the full URL — keeps dashboard cardinality manageable. Falls\n * back to `req.path` when no route matched (404s).\n * - Errors thrown by `getIdentity` are swallowed and the event still\n * ships without identity — telemetry must NEVER break the request\n * pipeline.\n */\nexport function crossdeckExpress(\n server: CrossdeckServer,\n options: CrossdeckExpressOptions = {},\n) {\n const skipPaths = options.skipPaths ?? DEFAULT_SKIP_PATHS;\n\n return function crossdeckExpressMiddleware(\n req: ExpressRequestLike,\n res: ExpressResponseLike,\n next: ExpressNext,\n ): void {\n if (shouldSkipRequest(req, skipPaths)) {\n next();\n return;\n }\n\n const start = Date.now();\n let dispatched = false;\n\n // Read-cost cross-match (the moat): stamp WHO for this request *before* the\n // route handler runs, so every database read it issues attributes to the\n // identified user in Buckets. We set the actor here at entry — the route\n // pattern isn't resolved until Express matches downstream, so WHAT is named\n // by the handler's own `bucket()` tags / SDK feature capture. No-op unless\n // the Buckets collector is installed; never throws into the request.\n try {\n let actor: string | undefined;\n try {\n actor = options.getIdentity?.(req)?.developerUserId;\n } catch {\n // identity extraction must never break the request pipeline\n }\n if (actor) bridgeReadCost({ actor });\n } catch {\n // the bridge is best-effort — a missing collector is a silent no-op\n }\n\n const emit = (): void => {\n if (dispatched) return;\n dispatched = true;\n let identity: ReturnType<NonNullable<CrossdeckExpressOptions[\"getIdentity\"]>> = undefined;\n try {\n identity = options.getIdentity?.(req);\n } catch {\n // identity extraction must never break the request pipeline\n }\n try {\n const props: Record<string, unknown> = {\n route: extractRoutePattern(req),\n method: req.method,\n statusCode: res.statusCode,\n durationMs: Date.now() - start,\n };\n // Defensive: every header read is in a try/swallow because a\n // misbehaving framework middleware may have mutated the\n // response in ways that throw on access.\n try {\n const ua = readHeader(req, \"user-agent\");\n if (ua) props.userAgent = ua;\n } catch {\n // skip\n }\n try {\n const cl = typeof res.getHeader === \"function\" ? res.getHeader(\"content-length\") : undefined;\n if (typeof cl === \"number\") {\n props.responseBytes = cl;\n } else if (typeof cl === \"string\") {\n const parsed = Number(cl);\n if (Number.isFinite(parsed)) props.responseBytes = parsed;\n }\n } catch {\n // skip\n }\n server.track({\n name: \"request.handled\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: props,\n });\n } catch {\n // SDK telemetry must never throw out of the response pipeline.\n }\n };\n\n res.once(\"finish\", emit);\n res.once(\"close\", emit);\n\n next();\n };\n}\n\n/**\n * Express error middleware (4-arg signature). Register LAST, after\n * all routes + after the request middleware:\n *\n * app.use(crossdeckExpressErrorHandler(server));\n *\n * Captures the error with request context, then forwards to the next\n * error handler — Crossdeck observes, the framework still produces the\n * normal 500 response.\n *\n * In Express 5 (async handlers natively forward errors), this middleware\n * sees errors from any handler. In Express 4, customers must wrap async\n * route handlers in a `next(err)` adapter — that's not a Crossdeck\n * limitation; it's how Express 4 works.\n */\nexport function crossdeckExpressErrorHandler(\n server: CrossdeckServer,\n options: CrossdeckExpressOptions = {},\n) {\n const attachContext = options.captureErrorsWithRequestContext !== false;\n\n return function crossdeckExpressErrorMiddleware(\n err: unknown,\n req: ExpressRequestLike,\n _res: ExpressResponseLike,\n next: ExpressNext,\n ): void {\n try {\n if (attachContext) {\n server.captureError(err, {\n context: {\n request: {\n url: req.originalUrl ?? req.url,\n method: req.method,\n route: extractRoutePattern(req),\n },\n },\n });\n } else {\n server.captureError(err);\n }\n } catch {\n // SDK observation must not block the framework's error pipeline.\n }\n next(err);\n };\n}\n\n// ---------- helpers (exported for testing) ----------\n\nexport function shouldSkipRequest(\n req: ExpressRequestLike,\n skipPaths: Array<string | RegExp>,\n): boolean {\n const candidates = [req.path, req.url].filter((s): s is string => typeof s === \"string\");\n for (const candidate of candidates) {\n for (const pattern of skipPaths) {\n if (typeof pattern === \"string\" && candidate.startsWith(pattern)) return true;\n if (pattern instanceof RegExp && pattern.test(candidate)) return true;\n }\n }\n return false;\n}\n\nfunction readHeader(req: ExpressRequestLike, name: string): string | undefined {\n if (!req.headers) return undefined;\n const v = req.headers[name];\n if (typeof v === \"string\") return v;\n if (Array.isArray(v)) return v[0];\n return undefined;\n}\n\nexport function extractRoutePattern(req: ExpressRequestLike): string {\n const routePath = req.route?.path;\n if (typeof routePath === \"string\") return routePath;\n if (routePath instanceof RegExp) return routePath.source;\n if (Array.isArray(routePath)) {\n return routePath\n .map((p) => (typeof p === \"string\" ? p : p instanceof RegExp ? p.source : \"\"))\n .filter(Boolean)\n .join(\"|\");\n }\n // Fallback for 404s + middleware-only requests where `req.route` is\n // undefined. Use `req.path` (the URL path without query string)\n // when present, else `req.url` as a last resort.\n return req.path ?? req.url ?? \"<unknown>\";\n}\n","/**\n * AWS Lambda handler wrapper — emits `function.invoked` /\n * `function.completed` / `function.failed` with Lambda lifecycle\n * metadata, and (crucially) `await server.flush()` BEFORE the handler\n * returns.\n *\n * Why flush-before-return is non-optional on Lambda: the runtime\n * freezes the process between invocations. Any event queued but not\n * sent over the wire vanishes — silently — when the function returns.\n * `flush-on-exit` doesn't fire because the process isn't exiting;\n * it's hibernating. Without the wrapper's explicit flush, you'd lose\n * the very telemetry you installed the SDK for.\n *\n * import { wrapLambdaHandler } from \"@cross-deck/node/auto-events\";\n *\n * export const handler = wrapLambdaHandler(server, async (event, ctx) => {\n * // your handler\n * });\n *\n * The wrapper preserves the handler's TypeScript signature via\n * generic parameters so the wrapped handler is type-equivalent to\n * the original.\n *\n * Cold-start detection is per-module-instance: the first invocation\n * gets `coldStart: true`, subsequent invocations of the SAME warm\n * container get `coldStart: false`. AWS spawns multiple containers\n * for concurrent invocations — each container's first invocation is\n * a cold start, so this is a per-container signal, not per-account.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\nimport { bridgeReadCost } from \"../read-cost-bridge\";\n\n/**\n * Minimal shape of the AWS Lambda invocation context. We don't pull\n * `@types/aws-lambda` as a dependency — that would force every\n * non-Lambda caller to install Lambda types just to import the SDK.\n * The fields we read are the stable subset every Lambda runtime\n * provides.\n */\nexport interface LambdaContextLike {\n awsRequestId?: string;\n functionName?: string;\n functionVersion?: string;\n invokedFunctionArn?: string;\n memoryLimitInMB?: number | string;\n logGroupName?: string;\n logStreamName?: string;\n /** Time remaining in the invocation, in ms. Useful for context. */\n getRemainingTimeInMillis?: () => number;\n}\n\nexport type LambdaHandlerLike<TEvent, TResult> = (\n event: TEvent,\n context: LambdaContextLike,\n) => Promise<TResult> | TResult;\n\nexport interface WrapLambdaOptions {\n /**\n * Override the per-container cold-start flag. Module-level\n * detection is sufficient for production; tests use this to\n * deterministically reset cold-start across runs.\n */\n resetColdStart?: boolean;\n /**\n * Optional identity extractor — read auth context from `event`\n * (e.g. `event.requestContext?.authorizer?.principalId` on an API\n * Gateway invocation) and attach to the emitted events.\n */\n getIdentity?: (event: unknown, context: LambdaContextLike) => {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n } | null | undefined;\n}\n\nlet containerColdStart = true;\n\n/**\n * Wrap a Lambda handler. Returns a handler with the same signature.\n *\n * Lifecycle emitted:\n * - `function.invoked` on entry — requestId, functionName, coldStart\n * - `function.completed` on success — durationMs, memoryUsedMb, statusCode\n * - `function.failed` on throw — errorType, errorMessage, durationMs\n *\n * Failures also call `server.captureError(err)` so the error pipeline\n * sees it with `error.handled` shape (frames + fingerprint +\n * breadcrumbs). The thrown error is re-thrown after capture so Lambda\n * itself still sees the failure and reports it to CloudWatch.\n *\n * `await server.flush()` runs in the `finally` block of every\n * invocation — bounded best-effort, so a transient backend outage\n * doesn't keep the function alive past the platform's SIGKILL.\n */\nexport function wrapLambdaHandler<TEvent, TResult>(\n server: CrossdeckServer,\n handler: LambdaHandlerLike<TEvent, TResult>,\n options: WrapLambdaOptions = {},\n): LambdaHandlerLike<TEvent, TResult> {\n if (options.resetColdStart === true) containerColdStart = true;\n\n return async function wrappedLambdaHandler(event, context): Promise<TResult> {\n const start = Date.now();\n const coldStart = containerColdStart;\n containerColdStart = false;\n const identity = safeExtractIdentity(options.getIdentity, event, context);\n\n // Read-cost cross-match: stamp WHO + WHAT for this invocation before the\n // handler runs, so its database reads attribute to the user and the function.\n // The function name IS the operation on serverless — a natural WHAT. Each\n // invocation is its own async context, so this never leaks across requests.\n // No-op unless @cross-deck/buckets is installed; never throws.\n try {\n bridgeReadCost({ actor: identity?.developerUserId, feature: context.functionName });\n } catch {\n // best-effort — a missing collector is a silent no-op\n }\n\n server.track({\n name: \"function.invoked\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n functionVersion: context.functionVersion,\n coldStart,\n memoryLimitMb: numericOrUndefined(context.memoryLimitInMB),\n remainingMs: safeRemainingMs(context),\n },\n });\n\n try {\n const result = await handler(event, context);\n const completedProps: Record<string, unknown> = {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n durationMs: Date.now() - start,\n memoryUsedMb: rssMb(),\n };\n // API Gateway / Function URL responses are\n // `{ statusCode, body, headers? }`. When the handler returns\n // that shape, surface statusCode + body size on the completed\n // event so the dashboard can pivot by HTTP outcome. Non-HTTP\n // handlers (queue / cron) return arbitrary shapes; we silently\n // skip those keys.\n if (isHttpStyleResponse(result)) {\n completedProps.statusCode = result.statusCode;\n if (typeof result.body === \"string\") {\n completedProps.responseBytes = Buffer.byteLength(result.body, \"utf8\");\n }\n }\n server.track({\n name: \"function.completed\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: completedProps,\n });\n return result;\n } catch (err) {\n try {\n server.captureError(err, {\n context: {\n lambda: {\n requestId: context.awsRequestId,\n functionName: context.functionName,\n functionVersion: context.functionVersion,\n },\n },\n });\n } catch {\n // self-protection — error capture must never block re-throw\n }\n try {\n server.track({\n name: \"function.failed\",\n developerUserId: identity?.developerUserId,\n anonymousId: identity?.anonymousId,\n crossdeckCustomerId: identity?.crossdeckCustomerId,\n properties: {\n runtime: \"aws-lambda\",\n requestId: context.awsRequestId,\n functionName: context.functionName,\n errorType: err instanceof Error ? err.name : null,\n errorMessage: err instanceof Error ? err.message : String(err),\n durationMs: Date.now() - start,\n },\n });\n } catch {\n // swallow — same self-protection\n }\n throw err;\n } finally {\n // CRITICAL — Lambda freezes the process between invocations.\n // Without this, queued events vanish silently the moment the\n // handler returns. `flush()` is best-effort + bounded by the\n // queue's own timeout policy.\n try {\n await server.flush();\n } catch {\n // Flush failure is observable via diagnostics.events.lastError.\n }\n }\n };\n}\n\nfunction safeExtractIdentity(\n extractor: WrapLambdaOptions[\"getIdentity\"] | undefined,\n event: unknown,\n context: LambdaContextLike,\n): ReturnType<NonNullable<WrapLambdaOptions[\"getIdentity\"]>> {\n if (!extractor) return undefined;\n try {\n return extractor(event, context);\n } catch {\n return undefined;\n }\n}\n\nfunction safeRemainingMs(context: LambdaContextLike): number | undefined {\n try {\n return typeof context.getRemainingTimeInMillis === \"function\"\n ? context.getRemainingTimeInMillis()\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction numericOrUndefined(value: number | string | undefined): number | undefined {\n if (typeof value === \"number\") return value;\n if (typeof value === \"string\") {\n const n = Number(value);\n return Number.isFinite(n) ? n : undefined;\n }\n return undefined;\n}\n\nfunction rssMb(): number {\n try {\n return Math.round(process.memoryUsage().rss / 1024 / 1024);\n } catch {\n return 0;\n }\n}\n\n/**\n * Duck-type detection for API Gateway / Function URL / ALB-style\n * Lambda responses. Real Lambda handlers can return ANY shape — only\n * HTTP-style returns expose statusCode + body. We don't import\n * `@types/aws-lambda` to avoid the dep cost; this duck-type is good\n * enough.\n */\nfunction isHttpStyleResponse(\n value: unknown,\n): value is { statusCode: number; body?: string; headers?: Record<string, string> } {\n return Boolean(\n value &&\n typeof value === \"object\" &&\n typeof (value as { statusCode?: unknown }).statusCode === \"number\",\n );\n}\n","/**\n * Firebase Cloud Functions wrapper — generic across v1 + v2, also\n * usable for Google Cloud Run Functions and Cloud Run services\n * (anything that exposes a Node handler and freezes / tears down\n * between invocations).\n *\n * Why generic: Firebase has many handler signatures across v1 and v2:\n * v1 https.onRequest: (req, res) => void\n * v1 https.onCall: (data, context) => Promise<result>\n * v1 firestore.onWrite: (snapshot, context) => Promise<void>\n * v1 pubsub.onPublish: (message, context) => Promise<void>\n * v2 onRequest: (req, res) => Promise<void>\n * v2 onCall: (request) => Promise<result>\n * v2 onDocumentWritten: (event) => Promise<void>\n *\n * Rather than ship one wrapper per signature (which would force a\n * dependency on `firebase-functions` types and break when Google\n * adds new triggers), this wrapper is **shape-preserving** — it\n * accepts ANY function and returns one with the same signature.\n * Lifecycle telemetry is emitted around the call; metadata extraction\n * is plug-in via `getMetadata`.\n *\n * import { wrapFunction } from \"@cross-deck/node/auto-events\";\n * import { onRequest } from \"firebase-functions/v2/https\";\n *\n * export const myFunction = onRequest(wrapFunction(server, async (req, res) => {\n * // your handler\n * }));\n *\n * Cold-start detection: same per-container logic as Lambda. The\n * first invocation of a fresh container is a cold start; subsequent\n * invocations of the same warm container are not.\n *\n * Flush-before-return: same critical contract as Lambda. Firebase\n * tears down idle containers; queued events vanish if the SDK doesn't\n * flush before the handler returns.\n */\n\nimport type { CrossdeckServer } from \"../crossdeck-server\";\n\nexport interface WrapFunctionOptions {\n /**\n * Override the per-container cold-start flag. Module-level\n * detection is sufficient for production; tests use this to\n * deterministically reset cold-start across runs.\n */\n resetColdStart?: boolean;\n /**\n * Optional metadata extractor — read trigger-specific fields off\n * the handler arguments and attach them to the emitted events.\n * Default: no extra metadata.\n *\n * Return `{ identity, properties }` so identity hints route onto\n * the event envelope (for dashboard pivot) and properties merge\n * into the event's `properties` bag:\n *\n * wrapFunction(server, handler, {\n * getMetadata: (args) => ({\n * identity: { developerUserId: args[0].auth?.uid },\n * properties: { docPath: args[0].ref?.path, region: \"us-central1\" },\n * }),\n * })\n */\n getMetadata?: (args: unknown[]) => WrapFunctionMetadata | null | undefined;\n /**\n * Label for the `runtime` event property. Defaults to\n * `\"firebase-functions\"`. Override to distinguish triggers in\n * dashboards (e.g. `\"firebase-https\"` vs `\"firebase-firestore\"`).\n */\n runtime?: string;\n}\n\nexport interface WrapFunctionMetadata {\n /** Identity hint attached to the event envelope (developerUserId / anonymousId / crossdeckCustomerId). */\n identity?: {\n developerUserId?: string;\n anonymousId?: string;\n crossdeckCustomerId?: string;\n };\n /** Additional properties merged into emitted event properties. */\n properties?: Record<string, unknown>;\n}\n\nlet containerColdStart = true;\n\n/**\n * Shape-preserving wrap for a Firebase / Cloud Run handler. Returns\n * a function with the SAME signature as the input.\n *\n * Lifecycle emitted:\n * - `function.invoked` on entry — runtime, coldStart, ...metadata\n * - `function.completed` on success — durationMs, memoryUsedMb\n * - `function.failed` on throw — errorType, errorMessage, durationMs\n *\n * Failures also call `server.captureError(err)`. Errors are re-thrown\n * so Firebase still sees the failure and reports it to Cloud Logging.\n *\n * `await server.flush()` runs in `finally` — same as Lambda. Firebase\n * containers freeze / tear down between invocations.\n */\nexport function wrapFunction<TArgs extends unknown[], TResult>(\n server: CrossdeckServer,\n handler: (...args: TArgs) => Promise<TResult> | TResult,\n options: WrapFunctionOptions = {},\n): (...args: TArgs) => Promise<TResult> {\n if (options.resetColdStart === true) containerColdStart = true;\n const runtime = options.runtime ?? \"firebase-functions\";\n\n return async function wrappedFirebaseHandler(...args: TArgs): Promise<TResult> {\n const start = Date.now();\n const coldStart = containerColdStart;\n containerColdStart = false;\n const metadata = safeExtractMetadata(options.getMetadata, args);\n const identity = metadata?.identity ?? {};\n const extraProps = metadata?.properties ?? {};\n\n server.track({\n name: \"function.invoked\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n coldStart,\n ...extraProps,\n },\n });\n\n try {\n const result = await handler(...args);\n server.track({\n name: \"function.completed\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n durationMs: Date.now() - start,\n memoryUsedMb: rssMb(),\n ...extraProps,\n },\n });\n return result;\n } catch (err) {\n try {\n server.captureError(err, {\n context: { firebase: extraProps },\n });\n } catch {\n // self-protection — error capture must never block re-throw\n }\n try {\n server.track({\n name: \"function.failed\",\n developerUserId: identity.developerUserId,\n anonymousId: identity.anonymousId,\n crossdeckCustomerId: identity.crossdeckCustomerId,\n properties: {\n runtime,\n errorType: err instanceof Error ? err.name : null,\n errorMessage: err instanceof Error ? err.message : String(err),\n durationMs: Date.now() - start,\n ...extraProps,\n },\n });\n } catch {\n // swallow — same self-protection\n }\n throw err;\n } finally {\n try {\n await server.flush();\n } catch {\n // Flush failure is observable via diagnostics.events.lastError.\n }\n }\n };\n}\n\nfunction safeExtractMetadata(\n extractor: WrapFunctionOptions[\"getMetadata\"] | undefined,\n args: unknown[],\n): WrapFunctionMetadata | undefined {\n if (!extractor) return undefined;\n try {\n const out = extractor(args);\n return out ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction rssMb(): number {\n try {\n return Math.round(process.memoryUsage().rss / 1024 / 1024);\n } catch {\n return 0;\n }\n}\n"],"mappings":"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","names":["containerColdStart","rssMb"]}
|
package/dist/index.cjs
CHANGED