@cross-deck/node 0.1.0 → 1.0.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 +116 -0
- package/README.md +406 -124
- package/dist/auto-events/index.cjs +354 -0
- package/dist/auto-events/index.cjs.map +1 -0
- package/dist/auto-events/index.d.mts +316 -0
- package/dist/auto-events/index.d.ts +316 -0
- package/dist/auto-events/index.mjs +322 -0
- package/dist/auto-events/index.mjs.map +1 -0
- package/dist/crossdeck-server-LvQwPKu5.d.mts +1393 -0
- package/dist/crossdeck-server-LvQwPKu5.d.ts +1393 -0
- package/dist/index.cjs +3058 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +339 -178
- package/dist/index.d.ts +339 -178
- package/dist/index.mjs +3043 -180
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -4
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { p as CrossdeckServer } from '../crossdeck-server-LvQwPKu5.mjs';
|
|
2
|
+
import 'node:events';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Express auto-events — `request.handled` middleware + uncaught-route
|
|
6
|
+
* error capture.
|
|
7
|
+
*
|
|
8
|
+
* Two middleware factories, registered separately because Express
|
|
9
|
+
* differentiates them by arity:
|
|
10
|
+
*
|
|
11
|
+
* import { crossdeckExpress, crossdeckExpressErrorHandler } from
|
|
12
|
+
* "@cross-deck/node/auto-events";
|
|
13
|
+
*
|
|
14
|
+
* app.use(crossdeckExpress(server)); // request middleware
|
|
15
|
+
* app.use(routes); // your routes
|
|
16
|
+
* app.use(crossdeckExpressErrorHandler(server)); // LAST — error middleware
|
|
17
|
+
*
|
|
18
|
+
* `crossdeckExpress` emits `request.handled` on response 'finish'
|
|
19
|
+
* with the matched route pattern (not the full URL — high-cardinality
|
|
20
|
+
* URL paths kill dashboards), method, statusCode, and durationMs.
|
|
21
|
+
*
|
|
22
|
+
* `crossdeckExpressErrorHandler` catches errors thrown in route
|
|
23
|
+
* handlers (sync OR async — Express 5 supports async handlers
|
|
24
|
+
* natively; Express 4 needs the caller to forward via `next(err)`).
|
|
25
|
+
* The error is shipped with request context (url, method, matched
|
|
26
|
+
* route) attached so the dashboard can group by route.
|
|
27
|
+
*
|
|
28
|
+
* Compatible with both Express 4 and Express 5. The middleware
|
|
29
|
+
* signatures are stable across both versions.
|
|
30
|
+
*
|
|
31
|
+
* No `import` from `express`. The adapter speaks shape-only against
|
|
32
|
+
* Express's request / response objects — customers don't pay a
|
|
33
|
+
* forced dependency on Express just to install the Crossdeck SDK.
|
|
34
|
+
* If `express` is missing at install, this module still compiles.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Shape of an Express request object — enough fields for the
|
|
39
|
+
* middleware to do its job without depending on Express's types.
|
|
40
|
+
*/
|
|
41
|
+
interface ExpressRequestLike {
|
|
42
|
+
method: string;
|
|
43
|
+
url: string;
|
|
44
|
+
path?: string;
|
|
45
|
+
route?: {
|
|
46
|
+
path?: string | RegExp | Array<string | RegExp>;
|
|
47
|
+
};
|
|
48
|
+
originalUrl?: string;
|
|
49
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
50
|
+
}
|
|
51
|
+
/** Shape of an Express response object. */
|
|
52
|
+
interface ExpressResponseLike {
|
|
53
|
+
statusCode: number;
|
|
54
|
+
once(event: "finish" | "close", listener: () => void): unknown;
|
|
55
|
+
/**
|
|
56
|
+
* Optional — Express's response exposes this for reading headers
|
|
57
|
+
* the framework / middleware chain set. Used by the middleware to
|
|
58
|
+
* surface `responseBytes` on the `request.handled` event. If your
|
|
59
|
+
* adapter doesn't have it, the field is simply omitted.
|
|
60
|
+
*/
|
|
61
|
+
getHeader?(name: string): string | string[] | number | undefined;
|
|
62
|
+
}
|
|
63
|
+
type ExpressNext = (err?: unknown) => void;
|
|
64
|
+
interface CrossdeckExpressOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Routes to skip. Tested against `req.route?.path` if available, else
|
|
67
|
+
* `req.path` / `req.url`. Defaults to a single self-skip for
|
|
68
|
+
* `/crossdeck/*` so the SDK doesn't emit telemetry about its own
|
|
69
|
+
* health endpoints.
|
|
70
|
+
*/
|
|
71
|
+
skipPaths?: Array<string | RegExp>;
|
|
72
|
+
/**
|
|
73
|
+
* Optional identity extractor — runs once per request. Whatever it
|
|
74
|
+
* returns is attached to the `request.handled` event so the
|
|
75
|
+
* dashboard can pivot by user. Typical implementation: read
|
|
76
|
+
* `req.user.id` populated by your auth middleware.
|
|
77
|
+
*
|
|
78
|
+
* crossdeckExpress(server, {
|
|
79
|
+
* getIdentity: (req) => ({ developerUserId: req.user?.id }),
|
|
80
|
+
* })
|
|
81
|
+
*/
|
|
82
|
+
getIdentity?: (req: ExpressRequestLike) => {
|
|
83
|
+
developerUserId?: string;
|
|
84
|
+
anonymousId?: string;
|
|
85
|
+
crossdeckCustomerId?: string;
|
|
86
|
+
} | null | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Attach `{ url, method, route }` as `context.request` on captured
|
|
89
|
+
* errors. Default `true`. Set `false` if you have a separate
|
|
90
|
+
* mechanism for capturing request context (Pino bindings, etc).
|
|
91
|
+
*/
|
|
92
|
+
captureErrorsWithRequestContext?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Express middleware that emits `request.handled` per request.
|
|
96
|
+
* Register BEFORE your routes:
|
|
97
|
+
*
|
|
98
|
+
* app.use(crossdeckExpress(server));
|
|
99
|
+
*
|
|
100
|
+
* Behaviour:
|
|
101
|
+
* - Listens on `res.once('finish')` so we capture the FINAL
|
|
102
|
+
* statusCode after any post-route middleware (compression, etc).
|
|
103
|
+
* Also listens on `res.once('close')` to cover client-aborted
|
|
104
|
+
* requests where 'finish' never fires.
|
|
105
|
+
* - Idempotent per request: dispatches once regardless of which
|
|
106
|
+
* terminal event fires first.
|
|
107
|
+
* - `route` property is the matched route PATTERN (`/users/:id`),
|
|
108
|
+
* not the full URL — keeps dashboard cardinality manageable. Falls
|
|
109
|
+
* back to `req.path` when no route matched (404s).
|
|
110
|
+
* - Errors thrown by `getIdentity` are swallowed and the event still
|
|
111
|
+
* ships without identity — telemetry must NEVER break the request
|
|
112
|
+
* pipeline.
|
|
113
|
+
*/
|
|
114
|
+
declare function crossdeckExpress(server: CrossdeckServer, options?: CrossdeckExpressOptions): (req: ExpressRequestLike, res: ExpressResponseLike, next: ExpressNext) => void;
|
|
115
|
+
/**
|
|
116
|
+
* Express error middleware (4-arg signature). Register LAST, after
|
|
117
|
+
* all routes + after the request middleware:
|
|
118
|
+
*
|
|
119
|
+
* app.use(crossdeckExpressErrorHandler(server));
|
|
120
|
+
*
|
|
121
|
+
* Captures the error with request context, then forwards to the next
|
|
122
|
+
* error handler — Crossdeck observes, the framework still produces the
|
|
123
|
+
* normal 500 response.
|
|
124
|
+
*
|
|
125
|
+
* In Express 5 (async handlers natively forward errors), this middleware
|
|
126
|
+
* sees errors from any handler. In Express 4, customers must wrap async
|
|
127
|
+
* route handlers in a `next(err)` adapter — that's not a Crossdeck
|
|
128
|
+
* limitation; it's how Express 4 works.
|
|
129
|
+
*/
|
|
130
|
+
declare function crossdeckExpressErrorHandler(server: CrossdeckServer, options?: CrossdeckExpressOptions): (err: unknown, req: ExpressRequestLike, _res: ExpressResponseLike, next: ExpressNext) => void;
|
|
131
|
+
declare function shouldSkipRequest(req: ExpressRequestLike, skipPaths: Array<string | RegExp>): boolean;
|
|
132
|
+
declare function extractRoutePattern(req: ExpressRequestLike): string;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* AWS Lambda handler wrapper — emits `function.invoked` /
|
|
136
|
+
* `function.completed` / `function.failed` with Lambda lifecycle
|
|
137
|
+
* metadata, and (crucially) `await server.flush()` BEFORE the handler
|
|
138
|
+
* returns.
|
|
139
|
+
*
|
|
140
|
+
* Why flush-before-return is non-optional on Lambda: the runtime
|
|
141
|
+
* freezes the process between invocations. Any event queued but not
|
|
142
|
+
* sent over the wire vanishes — silently — when the function returns.
|
|
143
|
+
* `flush-on-exit` doesn't fire because the process isn't exiting;
|
|
144
|
+
* it's hibernating. Without the wrapper's explicit flush, you'd lose
|
|
145
|
+
* the very telemetry you installed the SDK for.
|
|
146
|
+
*
|
|
147
|
+
* import { wrapLambdaHandler } from "@cross-deck/node/auto-events";
|
|
148
|
+
*
|
|
149
|
+
* export const handler = wrapLambdaHandler(server, async (event, ctx) => {
|
|
150
|
+
* // your handler
|
|
151
|
+
* });
|
|
152
|
+
*
|
|
153
|
+
* The wrapper preserves the handler's TypeScript signature via
|
|
154
|
+
* generic parameters so the wrapped handler is type-equivalent to
|
|
155
|
+
* the original.
|
|
156
|
+
*
|
|
157
|
+
* Cold-start detection is per-module-instance: the first invocation
|
|
158
|
+
* gets `coldStart: true`, subsequent invocations of the SAME warm
|
|
159
|
+
* container get `coldStart: false`. AWS spawns multiple containers
|
|
160
|
+
* for concurrent invocations — each container's first invocation is
|
|
161
|
+
* a cold start, so this is a per-container signal, not per-account.
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Minimal shape of the AWS Lambda invocation context. We don't pull
|
|
166
|
+
* `@types/aws-lambda` as a dependency — that would force every
|
|
167
|
+
* non-Lambda caller to install Lambda types just to import the SDK.
|
|
168
|
+
* The fields we read are the stable subset every Lambda runtime
|
|
169
|
+
* provides.
|
|
170
|
+
*/
|
|
171
|
+
interface LambdaContextLike {
|
|
172
|
+
awsRequestId?: string;
|
|
173
|
+
functionName?: string;
|
|
174
|
+
functionVersion?: string;
|
|
175
|
+
invokedFunctionArn?: string;
|
|
176
|
+
memoryLimitInMB?: number | string;
|
|
177
|
+
logGroupName?: string;
|
|
178
|
+
logStreamName?: string;
|
|
179
|
+
/** Time remaining in the invocation, in ms. Useful for context. */
|
|
180
|
+
getRemainingTimeInMillis?: () => number;
|
|
181
|
+
}
|
|
182
|
+
type LambdaHandlerLike<TEvent, TResult> = (event: TEvent, context: LambdaContextLike) => Promise<TResult> | TResult;
|
|
183
|
+
interface WrapLambdaOptions {
|
|
184
|
+
/**
|
|
185
|
+
* Override the per-container cold-start flag. Module-level
|
|
186
|
+
* detection is sufficient for production; tests use this to
|
|
187
|
+
* deterministically reset cold-start across runs.
|
|
188
|
+
*/
|
|
189
|
+
resetColdStart?: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Optional identity extractor — read auth context from `event`
|
|
192
|
+
* (e.g. `event.requestContext?.authorizer?.principalId` on an API
|
|
193
|
+
* Gateway invocation) and attach to the emitted events.
|
|
194
|
+
*/
|
|
195
|
+
getIdentity?: (event: unknown, context: LambdaContextLike) => {
|
|
196
|
+
developerUserId?: string;
|
|
197
|
+
anonymousId?: string;
|
|
198
|
+
crossdeckCustomerId?: string;
|
|
199
|
+
} | null | undefined;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Wrap a Lambda handler. Returns a handler with the same signature.
|
|
203
|
+
*
|
|
204
|
+
* Lifecycle emitted:
|
|
205
|
+
* - `function.invoked` on entry — requestId, functionName, coldStart
|
|
206
|
+
* - `function.completed` on success — durationMs, memoryUsedMb, statusCode
|
|
207
|
+
* - `function.failed` on throw — errorType, errorMessage, durationMs
|
|
208
|
+
*
|
|
209
|
+
* Failures also call `server.captureError(err)` so the error pipeline
|
|
210
|
+
* sees it with `error.handled` shape (frames + fingerprint +
|
|
211
|
+
* breadcrumbs). The thrown error is re-thrown after capture so Lambda
|
|
212
|
+
* itself still sees the failure and reports it to CloudWatch.
|
|
213
|
+
*
|
|
214
|
+
* `await server.flush()` runs in the `finally` block of every
|
|
215
|
+
* invocation — bounded best-effort, so a transient backend outage
|
|
216
|
+
* doesn't keep the function alive past the platform's SIGKILL.
|
|
217
|
+
*/
|
|
218
|
+
declare function wrapLambdaHandler<TEvent, TResult>(server: CrossdeckServer, handler: LambdaHandlerLike<TEvent, TResult>, options?: WrapLambdaOptions): LambdaHandlerLike<TEvent, TResult>;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Firebase Cloud Functions wrapper — generic across v1 + v2, also
|
|
222
|
+
* usable for Google Cloud Run Functions and Cloud Run services
|
|
223
|
+
* (anything that exposes a Node handler and freezes / tears down
|
|
224
|
+
* between invocations).
|
|
225
|
+
*
|
|
226
|
+
* Why generic: Firebase has many handler signatures across v1 and v2:
|
|
227
|
+
* v1 https.onRequest: (req, res) => void
|
|
228
|
+
* v1 https.onCall: (data, context) => Promise<result>
|
|
229
|
+
* v1 firestore.onWrite: (snapshot, context) => Promise<void>
|
|
230
|
+
* v1 pubsub.onPublish: (message, context) => Promise<void>
|
|
231
|
+
* v2 onRequest: (req, res) => Promise<void>
|
|
232
|
+
* v2 onCall: (request) => Promise<result>
|
|
233
|
+
* v2 onDocumentWritten: (event) => Promise<void>
|
|
234
|
+
*
|
|
235
|
+
* Rather than ship one wrapper per signature (which would force a
|
|
236
|
+
* dependency on `firebase-functions` types and break when Google
|
|
237
|
+
* adds new triggers), this wrapper is **shape-preserving** — it
|
|
238
|
+
* accepts ANY function and returns one with the same signature.
|
|
239
|
+
* Lifecycle telemetry is emitted around the call; metadata extraction
|
|
240
|
+
* is plug-in via `getMetadata`.
|
|
241
|
+
*
|
|
242
|
+
* import { wrapFunction } from "@cross-deck/node/auto-events";
|
|
243
|
+
* import { onRequest } from "firebase-functions/v2/https";
|
|
244
|
+
*
|
|
245
|
+
* export const myFunction = onRequest(wrapFunction(server, async (req, res) => {
|
|
246
|
+
* // your handler
|
|
247
|
+
* }));
|
|
248
|
+
*
|
|
249
|
+
* Cold-start detection: same per-container logic as Lambda. The
|
|
250
|
+
* first invocation of a fresh container is a cold start; subsequent
|
|
251
|
+
* invocations of the same warm container are not.
|
|
252
|
+
*
|
|
253
|
+
* Flush-before-return: same critical contract as Lambda. Firebase
|
|
254
|
+
* tears down idle containers; queued events vanish if the SDK doesn't
|
|
255
|
+
* flush before the handler returns.
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
interface WrapFunctionOptions {
|
|
259
|
+
/**
|
|
260
|
+
* Override the per-container cold-start flag. Module-level
|
|
261
|
+
* detection is sufficient for production; tests use this to
|
|
262
|
+
* deterministically reset cold-start across runs.
|
|
263
|
+
*/
|
|
264
|
+
resetColdStart?: boolean;
|
|
265
|
+
/**
|
|
266
|
+
* Optional metadata extractor — read trigger-specific fields off
|
|
267
|
+
* the handler arguments and attach them to the emitted events.
|
|
268
|
+
* Default: no extra metadata.
|
|
269
|
+
*
|
|
270
|
+
* Return `{ identity, properties }` so identity hints route onto
|
|
271
|
+
* the event envelope (for dashboard pivot) and properties merge
|
|
272
|
+
* into the event's `properties` bag:
|
|
273
|
+
*
|
|
274
|
+
* wrapFunction(server, handler, {
|
|
275
|
+
* getMetadata: (args) => ({
|
|
276
|
+
* identity: { developerUserId: args[0].auth?.uid },
|
|
277
|
+
* properties: { docPath: args[0].ref?.path, region: "us-central1" },
|
|
278
|
+
* }),
|
|
279
|
+
* })
|
|
280
|
+
*/
|
|
281
|
+
getMetadata?: (args: unknown[]) => WrapFunctionMetadata | null | undefined;
|
|
282
|
+
/**
|
|
283
|
+
* Label for the `runtime` event property. Defaults to
|
|
284
|
+
* `"firebase-functions"`. Override to distinguish triggers in
|
|
285
|
+
* dashboards (e.g. `"firebase-https"` vs `"firebase-firestore"`).
|
|
286
|
+
*/
|
|
287
|
+
runtime?: string;
|
|
288
|
+
}
|
|
289
|
+
interface WrapFunctionMetadata {
|
|
290
|
+
/** Identity hint attached to the event envelope (developerUserId / anonymousId / crossdeckCustomerId). */
|
|
291
|
+
identity?: {
|
|
292
|
+
developerUserId?: string;
|
|
293
|
+
anonymousId?: string;
|
|
294
|
+
crossdeckCustomerId?: string;
|
|
295
|
+
};
|
|
296
|
+
/** Additional properties merged into emitted event properties. */
|
|
297
|
+
properties?: Record<string, unknown>;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Shape-preserving wrap for a Firebase / Cloud Run handler. Returns
|
|
301
|
+
* a function with the SAME signature as the input.
|
|
302
|
+
*
|
|
303
|
+
* Lifecycle emitted:
|
|
304
|
+
* - `function.invoked` on entry — runtime, coldStart, ...metadata
|
|
305
|
+
* - `function.completed` on success — durationMs, memoryUsedMb
|
|
306
|
+
* - `function.failed` on throw — errorType, errorMessage, durationMs
|
|
307
|
+
*
|
|
308
|
+
* Failures also call `server.captureError(err)`. Errors are re-thrown
|
|
309
|
+
* so Firebase still sees the failure and reports it to Cloud Logging.
|
|
310
|
+
*
|
|
311
|
+
* `await server.flush()` runs in `finally` — same as Lambda. Firebase
|
|
312
|
+
* containers freeze / tear down between invocations.
|
|
313
|
+
*/
|
|
314
|
+
declare function wrapFunction<TArgs extends unknown[], TResult>(server: CrossdeckServer, handler: (...args: TArgs) => Promise<TResult> | TResult, options?: WrapFunctionOptions): (...args: TArgs) => Promise<TResult>;
|
|
315
|
+
|
|
316
|
+
export { type CrossdeckExpressOptions, type ExpressNext, type ExpressRequestLike, type ExpressResponseLike, type LambdaContextLike, type LambdaHandlerLike, type WrapFunctionMetadata, type WrapFunctionOptions, type WrapLambdaOptions, crossdeckExpress, crossdeckExpressErrorHandler, extractRoutePattern, shouldSkipRequest, wrapFunction, wrapLambdaHandler };
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { p as CrossdeckServer } from '../crossdeck-server-LvQwPKu5.js';
|
|
2
|
+
import 'node:events';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Express auto-events — `request.handled` middleware + uncaught-route
|
|
6
|
+
* error capture.
|
|
7
|
+
*
|
|
8
|
+
* Two middleware factories, registered separately because Express
|
|
9
|
+
* differentiates them by arity:
|
|
10
|
+
*
|
|
11
|
+
* import { crossdeckExpress, crossdeckExpressErrorHandler } from
|
|
12
|
+
* "@cross-deck/node/auto-events";
|
|
13
|
+
*
|
|
14
|
+
* app.use(crossdeckExpress(server)); // request middleware
|
|
15
|
+
* app.use(routes); // your routes
|
|
16
|
+
* app.use(crossdeckExpressErrorHandler(server)); // LAST — error middleware
|
|
17
|
+
*
|
|
18
|
+
* `crossdeckExpress` emits `request.handled` on response 'finish'
|
|
19
|
+
* with the matched route pattern (not the full URL — high-cardinality
|
|
20
|
+
* URL paths kill dashboards), method, statusCode, and durationMs.
|
|
21
|
+
*
|
|
22
|
+
* `crossdeckExpressErrorHandler` catches errors thrown in route
|
|
23
|
+
* handlers (sync OR async — Express 5 supports async handlers
|
|
24
|
+
* natively; Express 4 needs the caller to forward via `next(err)`).
|
|
25
|
+
* The error is shipped with request context (url, method, matched
|
|
26
|
+
* route) attached so the dashboard can group by route.
|
|
27
|
+
*
|
|
28
|
+
* Compatible with both Express 4 and Express 5. The middleware
|
|
29
|
+
* signatures are stable across both versions.
|
|
30
|
+
*
|
|
31
|
+
* No `import` from `express`. The adapter speaks shape-only against
|
|
32
|
+
* Express's request / response objects — customers don't pay a
|
|
33
|
+
* forced dependency on Express just to install the Crossdeck SDK.
|
|
34
|
+
* If `express` is missing at install, this module still compiles.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Shape of an Express request object — enough fields for the
|
|
39
|
+
* middleware to do its job without depending on Express's types.
|
|
40
|
+
*/
|
|
41
|
+
interface ExpressRequestLike {
|
|
42
|
+
method: string;
|
|
43
|
+
url: string;
|
|
44
|
+
path?: string;
|
|
45
|
+
route?: {
|
|
46
|
+
path?: string | RegExp | Array<string | RegExp>;
|
|
47
|
+
};
|
|
48
|
+
originalUrl?: string;
|
|
49
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
50
|
+
}
|
|
51
|
+
/** Shape of an Express response object. */
|
|
52
|
+
interface ExpressResponseLike {
|
|
53
|
+
statusCode: number;
|
|
54
|
+
once(event: "finish" | "close", listener: () => void): unknown;
|
|
55
|
+
/**
|
|
56
|
+
* Optional — Express's response exposes this for reading headers
|
|
57
|
+
* the framework / middleware chain set. Used by the middleware to
|
|
58
|
+
* surface `responseBytes` on the `request.handled` event. If your
|
|
59
|
+
* adapter doesn't have it, the field is simply omitted.
|
|
60
|
+
*/
|
|
61
|
+
getHeader?(name: string): string | string[] | number | undefined;
|
|
62
|
+
}
|
|
63
|
+
type ExpressNext = (err?: unknown) => void;
|
|
64
|
+
interface CrossdeckExpressOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Routes to skip. Tested against `req.route?.path` if available, else
|
|
67
|
+
* `req.path` / `req.url`. Defaults to a single self-skip for
|
|
68
|
+
* `/crossdeck/*` so the SDK doesn't emit telemetry about its own
|
|
69
|
+
* health endpoints.
|
|
70
|
+
*/
|
|
71
|
+
skipPaths?: Array<string | RegExp>;
|
|
72
|
+
/**
|
|
73
|
+
* Optional identity extractor — runs once per request. Whatever it
|
|
74
|
+
* returns is attached to the `request.handled` event so the
|
|
75
|
+
* dashboard can pivot by user. Typical implementation: read
|
|
76
|
+
* `req.user.id` populated by your auth middleware.
|
|
77
|
+
*
|
|
78
|
+
* crossdeckExpress(server, {
|
|
79
|
+
* getIdentity: (req) => ({ developerUserId: req.user?.id }),
|
|
80
|
+
* })
|
|
81
|
+
*/
|
|
82
|
+
getIdentity?: (req: ExpressRequestLike) => {
|
|
83
|
+
developerUserId?: string;
|
|
84
|
+
anonymousId?: string;
|
|
85
|
+
crossdeckCustomerId?: string;
|
|
86
|
+
} | null | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Attach `{ url, method, route }` as `context.request` on captured
|
|
89
|
+
* errors. Default `true`. Set `false` if you have a separate
|
|
90
|
+
* mechanism for capturing request context (Pino bindings, etc).
|
|
91
|
+
*/
|
|
92
|
+
captureErrorsWithRequestContext?: boolean;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Express middleware that emits `request.handled` per request.
|
|
96
|
+
* Register BEFORE your routes:
|
|
97
|
+
*
|
|
98
|
+
* app.use(crossdeckExpress(server));
|
|
99
|
+
*
|
|
100
|
+
* Behaviour:
|
|
101
|
+
* - Listens on `res.once('finish')` so we capture the FINAL
|
|
102
|
+
* statusCode after any post-route middleware (compression, etc).
|
|
103
|
+
* Also listens on `res.once('close')` to cover client-aborted
|
|
104
|
+
* requests where 'finish' never fires.
|
|
105
|
+
* - Idempotent per request: dispatches once regardless of which
|
|
106
|
+
* terminal event fires first.
|
|
107
|
+
* - `route` property is the matched route PATTERN (`/users/:id`),
|
|
108
|
+
* not the full URL — keeps dashboard cardinality manageable. Falls
|
|
109
|
+
* back to `req.path` when no route matched (404s).
|
|
110
|
+
* - Errors thrown by `getIdentity` are swallowed and the event still
|
|
111
|
+
* ships without identity — telemetry must NEVER break the request
|
|
112
|
+
* pipeline.
|
|
113
|
+
*/
|
|
114
|
+
declare function crossdeckExpress(server: CrossdeckServer, options?: CrossdeckExpressOptions): (req: ExpressRequestLike, res: ExpressResponseLike, next: ExpressNext) => void;
|
|
115
|
+
/**
|
|
116
|
+
* Express error middleware (4-arg signature). Register LAST, after
|
|
117
|
+
* all routes + after the request middleware:
|
|
118
|
+
*
|
|
119
|
+
* app.use(crossdeckExpressErrorHandler(server));
|
|
120
|
+
*
|
|
121
|
+
* Captures the error with request context, then forwards to the next
|
|
122
|
+
* error handler — Crossdeck observes, the framework still produces the
|
|
123
|
+
* normal 500 response.
|
|
124
|
+
*
|
|
125
|
+
* In Express 5 (async handlers natively forward errors), this middleware
|
|
126
|
+
* sees errors from any handler. In Express 4, customers must wrap async
|
|
127
|
+
* route handlers in a `next(err)` adapter — that's not a Crossdeck
|
|
128
|
+
* limitation; it's how Express 4 works.
|
|
129
|
+
*/
|
|
130
|
+
declare function crossdeckExpressErrorHandler(server: CrossdeckServer, options?: CrossdeckExpressOptions): (err: unknown, req: ExpressRequestLike, _res: ExpressResponseLike, next: ExpressNext) => void;
|
|
131
|
+
declare function shouldSkipRequest(req: ExpressRequestLike, skipPaths: Array<string | RegExp>): boolean;
|
|
132
|
+
declare function extractRoutePattern(req: ExpressRequestLike): string;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* AWS Lambda handler wrapper — emits `function.invoked` /
|
|
136
|
+
* `function.completed` / `function.failed` with Lambda lifecycle
|
|
137
|
+
* metadata, and (crucially) `await server.flush()` BEFORE the handler
|
|
138
|
+
* returns.
|
|
139
|
+
*
|
|
140
|
+
* Why flush-before-return is non-optional on Lambda: the runtime
|
|
141
|
+
* freezes the process between invocations. Any event queued but not
|
|
142
|
+
* sent over the wire vanishes — silently — when the function returns.
|
|
143
|
+
* `flush-on-exit` doesn't fire because the process isn't exiting;
|
|
144
|
+
* it's hibernating. Without the wrapper's explicit flush, you'd lose
|
|
145
|
+
* the very telemetry you installed the SDK for.
|
|
146
|
+
*
|
|
147
|
+
* import { wrapLambdaHandler } from "@cross-deck/node/auto-events";
|
|
148
|
+
*
|
|
149
|
+
* export const handler = wrapLambdaHandler(server, async (event, ctx) => {
|
|
150
|
+
* // your handler
|
|
151
|
+
* });
|
|
152
|
+
*
|
|
153
|
+
* The wrapper preserves the handler's TypeScript signature via
|
|
154
|
+
* generic parameters so the wrapped handler is type-equivalent to
|
|
155
|
+
* the original.
|
|
156
|
+
*
|
|
157
|
+
* Cold-start detection is per-module-instance: the first invocation
|
|
158
|
+
* gets `coldStart: true`, subsequent invocations of the SAME warm
|
|
159
|
+
* container get `coldStart: false`. AWS spawns multiple containers
|
|
160
|
+
* for concurrent invocations — each container's first invocation is
|
|
161
|
+
* a cold start, so this is a per-container signal, not per-account.
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Minimal shape of the AWS Lambda invocation context. We don't pull
|
|
166
|
+
* `@types/aws-lambda` as a dependency — that would force every
|
|
167
|
+
* non-Lambda caller to install Lambda types just to import the SDK.
|
|
168
|
+
* The fields we read are the stable subset every Lambda runtime
|
|
169
|
+
* provides.
|
|
170
|
+
*/
|
|
171
|
+
interface LambdaContextLike {
|
|
172
|
+
awsRequestId?: string;
|
|
173
|
+
functionName?: string;
|
|
174
|
+
functionVersion?: string;
|
|
175
|
+
invokedFunctionArn?: string;
|
|
176
|
+
memoryLimitInMB?: number | string;
|
|
177
|
+
logGroupName?: string;
|
|
178
|
+
logStreamName?: string;
|
|
179
|
+
/** Time remaining in the invocation, in ms. Useful for context. */
|
|
180
|
+
getRemainingTimeInMillis?: () => number;
|
|
181
|
+
}
|
|
182
|
+
type LambdaHandlerLike<TEvent, TResult> = (event: TEvent, context: LambdaContextLike) => Promise<TResult> | TResult;
|
|
183
|
+
interface WrapLambdaOptions {
|
|
184
|
+
/**
|
|
185
|
+
* Override the per-container cold-start flag. Module-level
|
|
186
|
+
* detection is sufficient for production; tests use this to
|
|
187
|
+
* deterministically reset cold-start across runs.
|
|
188
|
+
*/
|
|
189
|
+
resetColdStart?: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Optional identity extractor — read auth context from `event`
|
|
192
|
+
* (e.g. `event.requestContext?.authorizer?.principalId` on an API
|
|
193
|
+
* Gateway invocation) and attach to the emitted events.
|
|
194
|
+
*/
|
|
195
|
+
getIdentity?: (event: unknown, context: LambdaContextLike) => {
|
|
196
|
+
developerUserId?: string;
|
|
197
|
+
anonymousId?: string;
|
|
198
|
+
crossdeckCustomerId?: string;
|
|
199
|
+
} | null | undefined;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Wrap a Lambda handler. Returns a handler with the same signature.
|
|
203
|
+
*
|
|
204
|
+
* Lifecycle emitted:
|
|
205
|
+
* - `function.invoked` on entry — requestId, functionName, coldStart
|
|
206
|
+
* - `function.completed` on success — durationMs, memoryUsedMb, statusCode
|
|
207
|
+
* - `function.failed` on throw — errorType, errorMessage, durationMs
|
|
208
|
+
*
|
|
209
|
+
* Failures also call `server.captureError(err)` so the error pipeline
|
|
210
|
+
* sees it with `error.handled` shape (frames + fingerprint +
|
|
211
|
+
* breadcrumbs). The thrown error is re-thrown after capture so Lambda
|
|
212
|
+
* itself still sees the failure and reports it to CloudWatch.
|
|
213
|
+
*
|
|
214
|
+
* `await server.flush()` runs in the `finally` block of every
|
|
215
|
+
* invocation — bounded best-effort, so a transient backend outage
|
|
216
|
+
* doesn't keep the function alive past the platform's SIGKILL.
|
|
217
|
+
*/
|
|
218
|
+
declare function wrapLambdaHandler<TEvent, TResult>(server: CrossdeckServer, handler: LambdaHandlerLike<TEvent, TResult>, options?: WrapLambdaOptions): LambdaHandlerLike<TEvent, TResult>;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Firebase Cloud Functions wrapper — generic across v1 + v2, also
|
|
222
|
+
* usable for Google Cloud Run Functions and Cloud Run services
|
|
223
|
+
* (anything that exposes a Node handler and freezes / tears down
|
|
224
|
+
* between invocations).
|
|
225
|
+
*
|
|
226
|
+
* Why generic: Firebase has many handler signatures across v1 and v2:
|
|
227
|
+
* v1 https.onRequest: (req, res) => void
|
|
228
|
+
* v1 https.onCall: (data, context) => Promise<result>
|
|
229
|
+
* v1 firestore.onWrite: (snapshot, context) => Promise<void>
|
|
230
|
+
* v1 pubsub.onPublish: (message, context) => Promise<void>
|
|
231
|
+
* v2 onRequest: (req, res) => Promise<void>
|
|
232
|
+
* v2 onCall: (request) => Promise<result>
|
|
233
|
+
* v2 onDocumentWritten: (event) => Promise<void>
|
|
234
|
+
*
|
|
235
|
+
* Rather than ship one wrapper per signature (which would force a
|
|
236
|
+
* dependency on `firebase-functions` types and break when Google
|
|
237
|
+
* adds new triggers), this wrapper is **shape-preserving** — it
|
|
238
|
+
* accepts ANY function and returns one with the same signature.
|
|
239
|
+
* Lifecycle telemetry is emitted around the call; metadata extraction
|
|
240
|
+
* is plug-in via `getMetadata`.
|
|
241
|
+
*
|
|
242
|
+
* import { wrapFunction } from "@cross-deck/node/auto-events";
|
|
243
|
+
* import { onRequest } from "firebase-functions/v2/https";
|
|
244
|
+
*
|
|
245
|
+
* export const myFunction = onRequest(wrapFunction(server, async (req, res) => {
|
|
246
|
+
* // your handler
|
|
247
|
+
* }));
|
|
248
|
+
*
|
|
249
|
+
* Cold-start detection: same per-container logic as Lambda. The
|
|
250
|
+
* first invocation of a fresh container is a cold start; subsequent
|
|
251
|
+
* invocations of the same warm container are not.
|
|
252
|
+
*
|
|
253
|
+
* Flush-before-return: same critical contract as Lambda. Firebase
|
|
254
|
+
* tears down idle containers; queued events vanish if the SDK doesn't
|
|
255
|
+
* flush before the handler returns.
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
interface WrapFunctionOptions {
|
|
259
|
+
/**
|
|
260
|
+
* Override the per-container cold-start flag. Module-level
|
|
261
|
+
* detection is sufficient for production; tests use this to
|
|
262
|
+
* deterministically reset cold-start across runs.
|
|
263
|
+
*/
|
|
264
|
+
resetColdStart?: boolean;
|
|
265
|
+
/**
|
|
266
|
+
* Optional metadata extractor — read trigger-specific fields off
|
|
267
|
+
* the handler arguments and attach them to the emitted events.
|
|
268
|
+
* Default: no extra metadata.
|
|
269
|
+
*
|
|
270
|
+
* Return `{ identity, properties }` so identity hints route onto
|
|
271
|
+
* the event envelope (for dashboard pivot) and properties merge
|
|
272
|
+
* into the event's `properties` bag:
|
|
273
|
+
*
|
|
274
|
+
* wrapFunction(server, handler, {
|
|
275
|
+
* getMetadata: (args) => ({
|
|
276
|
+
* identity: { developerUserId: args[0].auth?.uid },
|
|
277
|
+
* properties: { docPath: args[0].ref?.path, region: "us-central1" },
|
|
278
|
+
* }),
|
|
279
|
+
* })
|
|
280
|
+
*/
|
|
281
|
+
getMetadata?: (args: unknown[]) => WrapFunctionMetadata | null | undefined;
|
|
282
|
+
/**
|
|
283
|
+
* Label for the `runtime` event property. Defaults to
|
|
284
|
+
* `"firebase-functions"`. Override to distinguish triggers in
|
|
285
|
+
* dashboards (e.g. `"firebase-https"` vs `"firebase-firestore"`).
|
|
286
|
+
*/
|
|
287
|
+
runtime?: string;
|
|
288
|
+
}
|
|
289
|
+
interface WrapFunctionMetadata {
|
|
290
|
+
/** Identity hint attached to the event envelope (developerUserId / anonymousId / crossdeckCustomerId). */
|
|
291
|
+
identity?: {
|
|
292
|
+
developerUserId?: string;
|
|
293
|
+
anonymousId?: string;
|
|
294
|
+
crossdeckCustomerId?: string;
|
|
295
|
+
};
|
|
296
|
+
/** Additional properties merged into emitted event properties. */
|
|
297
|
+
properties?: Record<string, unknown>;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Shape-preserving wrap for a Firebase / Cloud Run handler. Returns
|
|
301
|
+
* a function with the SAME signature as the input.
|
|
302
|
+
*
|
|
303
|
+
* Lifecycle emitted:
|
|
304
|
+
* - `function.invoked` on entry — runtime, coldStart, ...metadata
|
|
305
|
+
* - `function.completed` on success — durationMs, memoryUsedMb
|
|
306
|
+
* - `function.failed` on throw — errorType, errorMessage, durationMs
|
|
307
|
+
*
|
|
308
|
+
* Failures also call `server.captureError(err)`. Errors are re-thrown
|
|
309
|
+
* so Firebase still sees the failure and reports it to Cloud Logging.
|
|
310
|
+
*
|
|
311
|
+
* `await server.flush()` runs in `finally` — same as Lambda. Firebase
|
|
312
|
+
* containers freeze / tear down between invocations.
|
|
313
|
+
*/
|
|
314
|
+
declare function wrapFunction<TArgs extends unknown[], TResult>(server: CrossdeckServer, handler: (...args: TArgs) => Promise<TResult> | TResult, options?: WrapFunctionOptions): (...args: TArgs) => Promise<TResult>;
|
|
315
|
+
|
|
316
|
+
export { type CrossdeckExpressOptions, type ExpressNext, type ExpressRequestLike, type ExpressResponseLike, type LambdaContextLike, type LambdaHandlerLike, type WrapFunctionMetadata, type WrapFunctionOptions, type WrapLambdaOptions, crossdeckExpress, crossdeckExpressErrorHandler, extractRoutePattern, shouldSkipRequest, wrapFunction, wrapLambdaHandler };
|