@debugkit/ingest 0.0.0-stage → 0.2.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.
@@ -0,0 +1,148 @@
1
+ // src/index.ts
2
+ import { Envelope, parseStack } from "@debugkit/schema";
3
+
4
+ // src/fingerprint.ts
5
+ import { createHash } from "crypto";
6
+ function normaliseMessage(msg) {
7
+ return msg.replace(/https?:\/\/\S+/g, "<url>").replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, "<uuid>").replace(/\b[0-9a-f]{16,}\b/gi, "<hex>").replace(/[\w.+-]+@[\w-]+\.[\w.]+/g, "<email>").replace(/\d+(\.\d+)?/g, "<n>").replace(/(["'`]).*?\1/g, "<str>").trim().slice(0, 300);
8
+ }
9
+ function normaliseFile(file) {
10
+ if (!file) return "";
11
+ let f = file.replace(/\/\/&.*$/, "").replace(/[?#].*$/, "").replace(/^[a-z]+:\/\/[^/]+/i, "");
12
+ if (/\.(?:js)?bundle$/.test(f)) return f.replace(/^.*\//, "");
13
+ for (const root of ["/src/", "/lib/", "/app/"]) {
14
+ const i = f.lastIndexOf(root);
15
+ if (i >= 0) {
16
+ f = f.slice(i + 1);
17
+ break;
18
+ }
19
+ }
20
+ return f.replace(/\.[0-9a-f]{6,}\./g, ".");
21
+ }
22
+ function topInAppFrame(frames) {
23
+ return frames?.find((f) => f.inApp !== false && f.file) ?? frames?.[0];
24
+ }
25
+ function culpritOf(e) {
26
+ const f = topInAppFrame(e.error.frames);
27
+ if (!f) return void 0;
28
+ const loc = normaliseFile(f.file) + (f.line ? `:${f.line}` : "");
29
+ return f.fn ? `${f.fn} (${loc})` : loc;
30
+ }
31
+ function fingerprint(e) {
32
+ const f = topInAppFrame(e.error.frames);
33
+ const r = e.request;
34
+ const where = r ? `${r.method.toUpperCase()} ${r.url} ${r.status ? `${Math.floor(r.status / 100)}xx` : "failed"}` : f ? `${f.fn ?? "?"}@${normaliseFile(f.file)}` : normaliseMessage(e.error.message);
35
+ return createHash("sha256").update([e.error.name, where, e.flow ?? ""].join("|")).digest("hex").slice(0, 24);
36
+ }
37
+ function titleOf(e) {
38
+ return `${e.error.name}: ${e.error.message}`.slice(0, 200);
39
+ }
40
+
41
+ // src/index.ts
42
+ function parseIngestKeys(raw) {
43
+ const out = {};
44
+ for (const pair of (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean)) {
45
+ const [key, app] = pair.split(":");
46
+ if (key && app) out[key] = app;
47
+ }
48
+ return out;
49
+ }
50
+ var dayOf = (iso) => iso.slice(0, 10);
51
+ function sanitiseTs(ts, now) {
52
+ const t = Date.parse(ts);
53
+ const n = now.getTime();
54
+ if (Number.isNaN(t) || t > n + 5 * 6e4 || t < n - 7 * 864e5) return now.toISOString();
55
+ return new Date(t).toISOString();
56
+ }
57
+ function enrich(ctx) {
58
+ return {
59
+ ...ctx,
60
+ os: { ...ctx.os, name: ctx.os.name.toLowerCase() },
61
+ runtime: { ...ctx.runtime, name: ctx.runtime.name.toLowerCase() }
62
+ };
63
+ }
64
+ async function ingest(rawBody, ingestKey, opts) {
65
+ const now = opts.now?.() ?? /* @__PURE__ */ new Date();
66
+ if (!ingestKey || !opts.keys[ingestKey]) return { status: 401, body: { ok: false, error: "invalid ingest key" } };
67
+ if (Buffer.byteLength(rawBody) > (opts.maxBytes ?? 512 * 1024)) return { status: 413, body: { ok: false, error: "payload too large" } };
68
+ let json;
69
+ try {
70
+ json = JSON.parse(rawBody);
71
+ } catch {
72
+ return { status: 400, body: { ok: false, error: "invalid json" } };
73
+ }
74
+ const parsed = Envelope.safeParse(json);
75
+ if (!parsed.success) return { status: 400, body: { ok: false, error: "invalid envelope", issues: parsed.error.flatten() } };
76
+ const env = parsed.data;
77
+ const app = env.context.app.id;
78
+ if (opts.keys[ingestKey] !== app && opts.keys[ingestKey] !== "*") return { status: 403, body: { ok: false, error: `key not allowed for app ${app}` } };
79
+ const context = enrich(env.context);
80
+ const receivedAt = now.toISOString();
81
+ const stored = [];
82
+ const rollups = [];
83
+ const issues = /* @__PURE__ */ new Map();
84
+ for (const raw of env.events) {
85
+ const event = { ...raw, ts: sanitiseTs(raw.ts, now) };
86
+ if (event.type === "error" && !event.error.frames?.length && event.error.stack) event.error.frames = parseStack(event.error.stack);
87
+ const day = dayOf(event.ts);
88
+ if (event.type === "session") {
89
+ rollups.push({ app, day, dimension: "session", key: `${context.app.version || "unknown"}|${event.status}`, kind: "event", by: 1 });
90
+ continue;
91
+ }
92
+ const kind = event.type === "error" ? "error" : "event";
93
+ const inc = (dimension, key) => rollups.push({ app, day, dimension, key: key || "unknown", kind, by: 1 });
94
+ let fp;
95
+ if (event.type === "error") {
96
+ fp = fingerprint(event);
97
+ const cur = issues.get(fp);
98
+ const se = { app, day, fingerprint: fp, event, context, user: env.user, sessionId: env.session.id, receivedAt };
99
+ issues.set(fp, {
100
+ app,
101
+ fingerprint: fp,
102
+ title: titleOf(event),
103
+ culprit: culpritOf(event),
104
+ flow: event.flow,
105
+ by: (cur?.by ?? 0) + 1,
106
+ ts: cur && cur.ts > event.ts ? cur.ts : event.ts,
107
+ firstTs: cur && cur.firstTs < event.ts ? cur.firstTs : event.ts,
108
+ lastEvent: cur && cur.ts > event.ts ? cur.lastEvent : se
109
+ });
110
+ inc("issue", fp);
111
+ if (event.request) inc("endpoint", `${event.request.method.toUpperCase()} ${event.request.url}`);
112
+ rollups.push({ app, day: event.ts.slice(0, 13), dimension: "issueHour", key: fp, kind, by: 1 });
113
+ }
114
+ inc("total", "all");
115
+ inc("flow", event.flow ?? "untagged");
116
+ inc("os", `${context.os.name} ${context.os.version ?? ""}`.trim());
117
+ inc("appVersion", context.app.version);
118
+ inc("deviceModel", context.device.model);
119
+ if (event.type === "event" && event.outcome && event.flow) inc("flowOutcome", `${event.flow}|${event.outcome}`);
120
+ stored.push({ app, day, fingerprint: fp, event, context, user: env.user, sessionId: env.session.id, receivedAt });
121
+ }
122
+ await opts.store.registerApp(app);
123
+ await Promise.all([
124
+ stored.length ? opts.store.writeEvents(stored) : void 0,
125
+ opts.store.upsertIssues([...issues.values()]),
126
+ opts.store.incrementRollups(rollups)
127
+ ]);
128
+ return { status: 202, body: { ok: true, accepted: env.events.length } };
129
+ }
130
+ var CORS_HEADERS = {
131
+ "access-control-allow-origin": "*",
132
+ "access-control-allow-methods": "POST, OPTIONS",
133
+ "access-control-allow-headers": "content-type, x-debug-key",
134
+ "access-control-max-age": "86400"
135
+ };
136
+
137
+ export {
138
+ normaliseMessage,
139
+ normaliseFile,
140
+ topInAppFrame,
141
+ culpritOf,
142
+ fingerprint,
143
+ titleOf,
144
+ parseIngestKeys,
145
+ ingest,
146
+ CORS_HEADERS
147
+ };
148
+ //# sourceMappingURL=chunk-6C67KV5S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/fingerprint.ts"],"sourcesContent":["import { Envelope, parseStack, type Context, type Event, type RollupIncrement, type StoredEvent } from \"@debugkit/schema\";\nimport type { IssueUpsert, WriteStore } from \"@debugkit/adapters\";\nimport { culpritOf, fingerprint, titleOf } from \"./fingerprint\";\n\nexport * from \"./fingerprint\";\n\nexport interface IngestOptions {\n store: WriteStore;\n /**\n * Map of public ingest key → app id it may write to. A key can't write to another app.\n * The app id \"*\" lets a key write to any app (local development only).\n * Parse from env with `parseIngestKeys(process.env.DEBUG_INGEST_KEYS)`.\n */\n keys: Record<string, string>;\n /** Hard cap on accepted payload size in bytes. Default 512 KB. */\n maxBytes?: number;\n now?: () => Date;\n}\n\nexport interface IngestResult {\n status: number;\n body: { ok: boolean; accepted?: number; error?: string; issues?: unknown };\n}\n\n/** \"pk_abc:megaton-native,pk_def:other-app\" → { pk_abc: \"megaton-native\", ... } */\nexport function parseIngestKeys(raw: string | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n for (const pair of (raw ?? \"\").split(\",\").map((s) => s.trim()).filter(Boolean)) {\n const [key, app] = pair.split(\":\");\n if (key && app) out[key] = app;\n }\n return out;\n}\n\nconst dayOf = (iso: string) => iso.slice(0, 10);\n\n/** Clamp client clocks: events claiming to be from the future or very old are pinned to receipt time. */\nfunction sanitiseTs(ts: string, now: Date): string {\n const t = Date.parse(ts);\n const n = now.getTime();\n if (Number.isNaN(t) || t > n + 5 * 60_000 || t < n - 7 * 86_400_000) return now.toISOString();\n return new Date(t).toISOString();\n}\n\nfunction enrich(ctx: Context): Context {\n return {\n ...ctx,\n os: { ...ctx.os, name: ctx.os.name.toLowerCase() },\n runtime: { ...ctx.runtime, name: ctx.runtime.name.toLowerCase() },\n };\n}\n\n/** Platform-agnostic ingest. Wrap with ./firebase or ./lambda to deploy. */\nexport async function ingest(rawBody: string, ingestKey: string | undefined, opts: IngestOptions): Promise<IngestResult> {\n const now = opts.now?.() ?? new Date();\n if (!ingestKey || !opts.keys[ingestKey]) return { status: 401, body: { ok: false, error: \"invalid ingest key\" } };\n if (Buffer.byteLength(rawBody) > (opts.maxBytes ?? 512 * 1024)) return { status: 413, body: { ok: false, error: \"payload too large\" } };\n\n let json: unknown;\n try {\n json = JSON.parse(rawBody);\n } catch {\n return { status: 400, body: { ok: false, error: \"invalid json\" } };\n }\n const parsed = Envelope.safeParse(json);\n if (!parsed.success) return { status: 400, body: { ok: false, error: \"invalid envelope\", issues: parsed.error.flatten() } };\n\n const env = parsed.data;\n const app = env.context.app.id;\n if (opts.keys[ingestKey] !== app && opts.keys[ingestKey] !== \"*\") return { status: 403, body: { ok: false, error: `key not allowed for app ${app}` } };\n\n const context = enrich(env.context);\n const receivedAt = now.toISOString();\n const stored: StoredEvent[] = [];\n const rollups: RollupIncrement[] = [];\n const issues = new Map<string, IssueUpsert>();\n\n for (const raw of env.events) {\n const event = { ...raw, ts: sanitiseTs(raw.ts, now) } as Event;\n // Clients that don't parse stacks (curl, other SDKs) still get grouped by code location.\n if (event.type === \"error\" && !event.error.frames?.length && event.error.stack) event.error.frames = parseStack(event.error.stack);\n const day = dayOf(event.ts);\n // Session health only feeds the crash-free rollup; storing each one would cost a write per session.\n if (event.type === \"session\") {\n rollups.push({ app, day, dimension: \"session\", key: `${context.app.version || \"unknown\"}|${event.status}`, kind: \"event\", by: 1 });\n continue;\n }\n const kind = event.type === \"error\" ? \"error\" : \"event\";\n const inc = (dimension: RollupIncrement[\"dimension\"], key: string | undefined) =>\n rollups.push({ app, day, dimension, key: key || \"unknown\", kind, by: 1 });\n\n let fp: string | undefined;\n if (event.type === \"error\") {\n fp = fingerprint(event);\n const cur = issues.get(fp);\n const se: StoredEvent = { app, day, fingerprint: fp, event, context, user: env.user, sessionId: env.session.id, receivedAt };\n issues.set(fp, {\n app,\n fingerprint: fp,\n title: titleOf(event),\n culprit: culpritOf(event),\n flow: event.flow,\n by: (cur?.by ?? 0) + 1,\n ts: cur && cur.ts > event.ts ? cur.ts : event.ts,\n firstTs: cur && cur.firstTs < event.ts ? cur.firstTs : event.ts,\n lastEvent: cur && cur.ts > event.ts ? cur.lastEvent : se,\n });\n inc(\"issue\", fp);\n if (event.request) inc(\"endpoint\", `${event.request.method.toUpperCase()} ${event.request.url}`);\n rollups.push({ app, day: event.ts.slice(0, 13), dimension: \"issueHour\", key: fp, kind, by: 1 });\n }\n\n inc(\"total\", \"all\");\n inc(\"flow\", event.flow ?? \"untagged\");\n inc(\"os\", `${context.os.name} ${context.os.version ?? \"\"}`.trim());\n inc(\"appVersion\", context.app.version);\n inc(\"deviceModel\", context.device.model);\n if (event.type === \"event\" && event.outcome && event.flow) inc(\"flowOutcome\", `${event.flow}|${event.outcome}`);\n\n stored.push({ app, day, fingerprint: fp, event, context, user: env.user, sessionId: env.session.id, receivedAt });\n }\n\n await opts.store.registerApp(app);\n await Promise.all([\n stored.length ? opts.store.writeEvents(stored) : undefined,\n opts.store.upsertIssues([...issues.values()]),\n opts.store.incrementRollups(rollups),\n ]);\n\n return { status: 202, body: { ok: true, accepted: env.events.length } };\n}\n\nexport const CORS_HEADERS = {\n \"access-control-allow-origin\": \"*\",\n \"access-control-allow-methods\": \"POST, OPTIONS\",\n \"access-control-allow-headers\": \"content-type, x-debug-key\",\n \"access-control-max-age\": \"86400\",\n};\n","import { createHash } from \"node:crypto\";\nimport type { ErrorEvent, StackFrame } from \"@debugkit/schema\";\n\n/** Strip volatile parts so \"User 123 not found\" and \"User 456 not found\" group together. */\nexport function normaliseMessage(msg: string): string {\n return msg\n .replace(/https?:\\/\\/\\S+/g, \"<url>\")\n .replace(/\\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\b/gi, \"<uuid>\")\n .replace(/\\b[0-9a-f]{16,}\\b/gi, \"<hex>\")\n .replace(/[\\w.+-]+@[\\w-]+\\.[\\w.]+/g, \"<email>\")\n .replace(/\\d+(\\.\\d+)?/g, \"<n>\")\n .replace(/([\"'`]).*?\\1/g, \"<str>\")\n .trim()\n .slice(0, 300);\n}\n\n/** Remove build hashes / absolute prefixes so the same code location matches across builds and devices. */\nexport function normaliseFile(file: string | undefined): string {\n if (!file) return \"\";\n // Metro dev bundles put options after \"//&\" instead of \"?\"; drop both.\n let f = file.replace(/\\/\\/&.*$/, \"\").replace(/[?#].*$/, \"\").replace(/^[a-z]+:\\/\\/[^/]+/i, \"\");\n // React Native bundles: the path is noise, the bundle name is enough.\n if (/\\.(?:js)?bundle$/.test(f)) return f.replace(/^.*\\//, \"\");\n // Keep the path from the deepest conventional source root, preferring src/.\n for (const root of [\"/src/\", \"/lib/\", \"/app/\"]) {\n const i = f.lastIndexOf(root);\n if (i >= 0) {\n f = f.slice(i + 1);\n break;\n }\n }\n return f.replace(/\\.[0-9a-f]{6,}\\./g, \".\");\n}\n\nexport function topInAppFrame(frames: StackFrame[] | undefined): StackFrame | undefined {\n return frames?.find((f) => f.inApp !== false && f.file) ?? frames?.[0];\n}\n\nexport function culpritOf(e: ErrorEvent): string | undefined {\n const f = topInAppFrame(e.error.frames);\n if (!f) return undefined;\n const loc = normaliseFile(f.file) + (f.line ? `:${f.line}` : \"\");\n return f.fn ? `${f.fn} (${loc})` : loc;\n}\n\n/**\n * Fingerprint = error name + top in-app frame (function + file, not line, so small edits\n * don't split issues) + flow. Falls back to the normalised message when there's no stack.\n */\nexport function fingerprint(e: ErrorEvent): string {\n const f = topInAppFrame(e.error.frames);\n // Failed requests group by endpoint and status class: every 503 from /v1/feed is one issue,\n // whichever screen made the call.\n const r = e.request;\n const where = r\n ? `${r.method.toUpperCase()} ${r.url} ${r.status ? `${Math.floor(r.status / 100)}xx` : \"failed\"}`\n : f\n ? `${f.fn ?? \"?\"}@${normaliseFile(f.file)}`\n : normaliseMessage(e.error.message);\n return createHash(\"sha256\")\n .update([e.error.name, where, e.flow ?? \"\"].join(\"|\"))\n .digest(\"hex\")\n .slice(0, 24);\n}\n\nexport function titleOf(e: ErrorEvent): string {\n return `${e.error.name}: ${e.error.message}`.slice(0, 200);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,148 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/index.ts
2
+ var _schema = require('@debugkit/schema');
3
+
4
+ // src/fingerprint.ts
5
+ var _crypto = require('crypto');
6
+ function normaliseMessage(msg) {
7
+ return msg.replace(/https?:\/\/\S+/g, "<url>").replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, "<uuid>").replace(/\b[0-9a-f]{16,}\b/gi, "<hex>").replace(/[\w.+-]+@[\w-]+\.[\w.]+/g, "<email>").replace(/\d+(\.\d+)?/g, "<n>").replace(/(["'`]).*?\1/g, "<str>").trim().slice(0, 300);
8
+ }
9
+ function normaliseFile(file) {
10
+ if (!file) return "";
11
+ let f = file.replace(/\/\/&.*$/, "").replace(/[?#].*$/, "").replace(/^[a-z]+:\/\/[^/]+/i, "");
12
+ if (/\.(?:js)?bundle$/.test(f)) return f.replace(/^.*\//, "");
13
+ for (const root of ["/src/", "/lib/", "/app/"]) {
14
+ const i = f.lastIndexOf(root);
15
+ if (i >= 0) {
16
+ f = f.slice(i + 1);
17
+ break;
18
+ }
19
+ }
20
+ return f.replace(/\.[0-9a-f]{6,}\./g, ".");
21
+ }
22
+ function topInAppFrame(frames) {
23
+ return _nullishCoalesce(_optionalChain([frames, 'optionalAccess', _ => _.find, 'call', _2 => _2((f) => f.inApp !== false && f.file)]), () => ( _optionalChain([frames, 'optionalAccess', _3 => _3[0]])));
24
+ }
25
+ function culpritOf(e) {
26
+ const f = topInAppFrame(e.error.frames);
27
+ if (!f) return void 0;
28
+ const loc = normaliseFile(f.file) + (f.line ? `:${f.line}` : "");
29
+ return f.fn ? `${f.fn} (${loc})` : loc;
30
+ }
31
+ function fingerprint(e) {
32
+ const f = topInAppFrame(e.error.frames);
33
+ const r = e.request;
34
+ const where = r ? `${r.method.toUpperCase()} ${r.url} ${r.status ? `${Math.floor(r.status / 100)}xx` : "failed"}` : f ? `${_nullishCoalesce(f.fn, () => ( "?"))}@${normaliseFile(f.file)}` : normaliseMessage(e.error.message);
35
+ return _crypto.createHash.call(void 0, "sha256").update([e.error.name, where, _nullishCoalesce(e.flow, () => ( ""))].join("|")).digest("hex").slice(0, 24);
36
+ }
37
+ function titleOf(e) {
38
+ return `${e.error.name}: ${e.error.message}`.slice(0, 200);
39
+ }
40
+
41
+ // src/index.ts
42
+ function parseIngestKeys(raw) {
43
+ const out = {};
44
+ for (const pair of (_nullishCoalesce(raw, () => ( ""))).split(",").map((s) => s.trim()).filter(Boolean)) {
45
+ const [key, app] = pair.split(":");
46
+ if (key && app) out[key] = app;
47
+ }
48
+ return out;
49
+ }
50
+ var dayOf = (iso) => iso.slice(0, 10);
51
+ function sanitiseTs(ts, now) {
52
+ const t = Date.parse(ts);
53
+ const n = now.getTime();
54
+ if (Number.isNaN(t) || t > n + 5 * 6e4 || t < n - 7 * 864e5) return now.toISOString();
55
+ return new Date(t).toISOString();
56
+ }
57
+ function enrich(ctx) {
58
+ return {
59
+ ...ctx,
60
+ os: { ...ctx.os, name: ctx.os.name.toLowerCase() },
61
+ runtime: { ...ctx.runtime, name: ctx.runtime.name.toLowerCase() }
62
+ };
63
+ }
64
+ async function ingest(rawBody, ingestKey, opts) {
65
+ const now = _nullishCoalesce(_optionalChain([opts, 'access', _4 => _4.now, 'optionalCall', _5 => _5()]), () => ( /* @__PURE__ */ new Date()));
66
+ if (!ingestKey || !opts.keys[ingestKey]) return { status: 401, body: { ok: false, error: "invalid ingest key" } };
67
+ if (Buffer.byteLength(rawBody) > (_nullishCoalesce(opts.maxBytes, () => ( 512 * 1024)))) return { status: 413, body: { ok: false, error: "payload too large" } };
68
+ let json;
69
+ try {
70
+ json = JSON.parse(rawBody);
71
+ } catch (e2) {
72
+ return { status: 400, body: { ok: false, error: "invalid json" } };
73
+ }
74
+ const parsed = _schema.Envelope.safeParse(json);
75
+ if (!parsed.success) return { status: 400, body: { ok: false, error: "invalid envelope", issues: parsed.error.flatten() } };
76
+ const env = parsed.data;
77
+ const app = env.context.app.id;
78
+ if (opts.keys[ingestKey] !== app && opts.keys[ingestKey] !== "*") return { status: 403, body: { ok: false, error: `key not allowed for app ${app}` } };
79
+ const context = enrich(env.context);
80
+ const receivedAt = now.toISOString();
81
+ const stored = [];
82
+ const rollups = [];
83
+ const issues = /* @__PURE__ */ new Map();
84
+ for (const raw of env.events) {
85
+ const event = { ...raw, ts: sanitiseTs(raw.ts, now) };
86
+ if (event.type === "error" && !_optionalChain([event, 'access', _6 => _6.error, 'access', _7 => _7.frames, 'optionalAccess', _8 => _8.length]) && event.error.stack) event.error.frames = _schema.parseStack.call(void 0, event.error.stack);
87
+ const day = dayOf(event.ts);
88
+ if (event.type === "session") {
89
+ rollups.push({ app, day, dimension: "session", key: `${context.app.version || "unknown"}|${event.status}`, kind: "event", by: 1 });
90
+ continue;
91
+ }
92
+ const kind = event.type === "error" ? "error" : "event";
93
+ const inc = (dimension, key) => rollups.push({ app, day, dimension, key: key || "unknown", kind, by: 1 });
94
+ let fp;
95
+ if (event.type === "error") {
96
+ fp = fingerprint(event);
97
+ const cur = issues.get(fp);
98
+ const se = { app, day, fingerprint: fp, event, context, user: env.user, sessionId: env.session.id, receivedAt };
99
+ issues.set(fp, {
100
+ app,
101
+ fingerprint: fp,
102
+ title: titleOf(event),
103
+ culprit: culpritOf(event),
104
+ flow: event.flow,
105
+ by: (_nullishCoalesce(_optionalChain([cur, 'optionalAccess', _9 => _9.by]), () => ( 0))) + 1,
106
+ ts: cur && cur.ts > event.ts ? cur.ts : event.ts,
107
+ firstTs: cur && cur.firstTs < event.ts ? cur.firstTs : event.ts,
108
+ lastEvent: cur && cur.ts > event.ts ? cur.lastEvent : se
109
+ });
110
+ inc("issue", fp);
111
+ if (event.request) inc("endpoint", `${event.request.method.toUpperCase()} ${event.request.url}`);
112
+ rollups.push({ app, day: event.ts.slice(0, 13), dimension: "issueHour", key: fp, kind, by: 1 });
113
+ }
114
+ inc("total", "all");
115
+ inc("flow", _nullishCoalesce(event.flow, () => ( "untagged")));
116
+ inc("os", `${context.os.name} ${_nullishCoalesce(context.os.version, () => ( ""))}`.trim());
117
+ inc("appVersion", context.app.version);
118
+ inc("deviceModel", context.device.model);
119
+ if (event.type === "event" && event.outcome && event.flow) inc("flowOutcome", `${event.flow}|${event.outcome}`);
120
+ stored.push({ app, day, fingerprint: fp, event, context, user: env.user, sessionId: env.session.id, receivedAt });
121
+ }
122
+ await opts.store.registerApp(app);
123
+ await Promise.all([
124
+ stored.length ? opts.store.writeEvents(stored) : void 0,
125
+ opts.store.upsertIssues([...issues.values()]),
126
+ opts.store.incrementRollups(rollups)
127
+ ]);
128
+ return { status: 202, body: { ok: true, accepted: env.events.length } };
129
+ }
130
+ var CORS_HEADERS = {
131
+ "access-control-allow-origin": "*",
132
+ "access-control-allow-methods": "POST, OPTIONS",
133
+ "access-control-allow-headers": "content-type, x-debug-key",
134
+ "access-control-max-age": "86400"
135
+ };
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
144
+
145
+
146
+
147
+ exports.normaliseMessage = normaliseMessage; exports.normaliseFile = normaliseFile; exports.topInAppFrame = topInAppFrame; exports.culpritOf = culpritOf; exports.fingerprint = fingerprint; exports.titleOf = titleOf; exports.parseIngestKeys = parseIngestKeys; exports.ingest = ingest; exports.CORS_HEADERS = CORS_HEADERS;
148
+ //# sourceMappingURL=chunk-YSIB36FS.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/sivemwanda/Work/Tanstack/deBug/packages/ingest/dist/chunk-YSIB36FS.cjs","../src/index.ts","../src/fingerprint.ts"],"names":[],"mappings":"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","file":"/Users/sivemwanda/Work/Tanstack/deBug/packages/ingest/dist/chunk-YSIB36FS.cjs","sourcesContent":[null,"import { Envelope, parseStack, type Context, type Event, type RollupIncrement, type StoredEvent } from \"@debugkit/schema\";\nimport type { IssueUpsert, WriteStore } from \"@debugkit/adapters\";\nimport { culpritOf, fingerprint, titleOf } from \"./fingerprint\";\n\nexport * from \"./fingerprint\";\n\nexport interface IngestOptions {\n store: WriteStore;\n /**\n * Map of public ingest key → app id it may write to. A key can't write to another app.\n * The app id \"*\" lets a key write to any app (local development only).\n * Parse from env with `parseIngestKeys(process.env.DEBUG_INGEST_KEYS)`.\n */\n keys: Record<string, string>;\n /** Hard cap on accepted payload size in bytes. Default 512 KB. */\n maxBytes?: number;\n now?: () => Date;\n}\n\nexport interface IngestResult {\n status: number;\n body: { ok: boolean; accepted?: number; error?: string; issues?: unknown };\n}\n\n/** \"pk_abc:megaton-native,pk_def:other-app\" → { pk_abc: \"megaton-native\", ... } */\nexport function parseIngestKeys(raw: string | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n for (const pair of (raw ?? \"\").split(\",\").map((s) => s.trim()).filter(Boolean)) {\n const [key, app] = pair.split(\":\");\n if (key && app) out[key] = app;\n }\n return out;\n}\n\nconst dayOf = (iso: string) => iso.slice(0, 10);\n\n/** Clamp client clocks: events claiming to be from the future or very old are pinned to receipt time. */\nfunction sanitiseTs(ts: string, now: Date): string {\n const t = Date.parse(ts);\n const n = now.getTime();\n if (Number.isNaN(t) || t > n + 5 * 60_000 || t < n - 7 * 86_400_000) return now.toISOString();\n return new Date(t).toISOString();\n}\n\nfunction enrich(ctx: Context): Context {\n return {\n ...ctx,\n os: { ...ctx.os, name: ctx.os.name.toLowerCase() },\n runtime: { ...ctx.runtime, name: ctx.runtime.name.toLowerCase() },\n };\n}\n\n/** Platform-agnostic ingest. Wrap with ./firebase or ./lambda to deploy. */\nexport async function ingest(rawBody: string, ingestKey: string | undefined, opts: IngestOptions): Promise<IngestResult> {\n const now = opts.now?.() ?? new Date();\n if (!ingestKey || !opts.keys[ingestKey]) return { status: 401, body: { ok: false, error: \"invalid ingest key\" } };\n if (Buffer.byteLength(rawBody) > (opts.maxBytes ?? 512 * 1024)) return { status: 413, body: { ok: false, error: \"payload too large\" } };\n\n let json: unknown;\n try {\n json = JSON.parse(rawBody);\n } catch {\n return { status: 400, body: { ok: false, error: \"invalid json\" } };\n }\n const parsed = Envelope.safeParse(json);\n if (!parsed.success) return { status: 400, body: { ok: false, error: \"invalid envelope\", issues: parsed.error.flatten() } };\n\n const env = parsed.data;\n const app = env.context.app.id;\n if (opts.keys[ingestKey] !== app && opts.keys[ingestKey] !== \"*\") return { status: 403, body: { ok: false, error: `key not allowed for app ${app}` } };\n\n const context = enrich(env.context);\n const receivedAt = now.toISOString();\n const stored: StoredEvent[] = [];\n const rollups: RollupIncrement[] = [];\n const issues = new Map<string, IssueUpsert>();\n\n for (const raw of env.events) {\n const event = { ...raw, ts: sanitiseTs(raw.ts, now) } as Event;\n // Clients that don't parse stacks (curl, other SDKs) still get grouped by code location.\n if (event.type === \"error\" && !event.error.frames?.length && event.error.stack) event.error.frames = parseStack(event.error.stack);\n const day = dayOf(event.ts);\n // Session health only feeds the crash-free rollup; storing each one would cost a write per session.\n if (event.type === \"session\") {\n rollups.push({ app, day, dimension: \"session\", key: `${context.app.version || \"unknown\"}|${event.status}`, kind: \"event\", by: 1 });\n continue;\n }\n const kind = event.type === \"error\" ? \"error\" : \"event\";\n const inc = (dimension: RollupIncrement[\"dimension\"], key: string | undefined) =>\n rollups.push({ app, day, dimension, key: key || \"unknown\", kind, by: 1 });\n\n let fp: string | undefined;\n if (event.type === \"error\") {\n fp = fingerprint(event);\n const cur = issues.get(fp);\n const se: StoredEvent = { app, day, fingerprint: fp, event, context, user: env.user, sessionId: env.session.id, receivedAt };\n issues.set(fp, {\n app,\n fingerprint: fp,\n title: titleOf(event),\n culprit: culpritOf(event),\n flow: event.flow,\n by: (cur?.by ?? 0) + 1,\n ts: cur && cur.ts > event.ts ? cur.ts : event.ts,\n firstTs: cur && cur.firstTs < event.ts ? cur.firstTs : event.ts,\n lastEvent: cur && cur.ts > event.ts ? cur.lastEvent : se,\n });\n inc(\"issue\", fp);\n if (event.request) inc(\"endpoint\", `${event.request.method.toUpperCase()} ${event.request.url}`);\n rollups.push({ app, day: event.ts.slice(0, 13), dimension: \"issueHour\", key: fp, kind, by: 1 });\n }\n\n inc(\"total\", \"all\");\n inc(\"flow\", event.flow ?? \"untagged\");\n inc(\"os\", `${context.os.name} ${context.os.version ?? \"\"}`.trim());\n inc(\"appVersion\", context.app.version);\n inc(\"deviceModel\", context.device.model);\n if (event.type === \"event\" && event.outcome && event.flow) inc(\"flowOutcome\", `${event.flow}|${event.outcome}`);\n\n stored.push({ app, day, fingerprint: fp, event, context, user: env.user, sessionId: env.session.id, receivedAt });\n }\n\n await opts.store.registerApp(app);\n await Promise.all([\n stored.length ? opts.store.writeEvents(stored) : undefined,\n opts.store.upsertIssues([...issues.values()]),\n opts.store.incrementRollups(rollups),\n ]);\n\n return { status: 202, body: { ok: true, accepted: env.events.length } };\n}\n\nexport const CORS_HEADERS = {\n \"access-control-allow-origin\": \"*\",\n \"access-control-allow-methods\": \"POST, OPTIONS\",\n \"access-control-allow-headers\": \"content-type, x-debug-key\",\n \"access-control-max-age\": \"86400\",\n};\n","import { createHash } from \"node:crypto\";\nimport type { ErrorEvent, StackFrame } from \"@debugkit/schema\";\n\n/** Strip volatile parts so \"User 123 not found\" and \"User 456 not found\" group together. */\nexport function normaliseMessage(msg: string): string {\n return msg\n .replace(/https?:\\/\\/\\S+/g, \"<url>\")\n .replace(/\\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\b/gi, \"<uuid>\")\n .replace(/\\b[0-9a-f]{16,}\\b/gi, \"<hex>\")\n .replace(/[\\w.+-]+@[\\w-]+\\.[\\w.]+/g, \"<email>\")\n .replace(/\\d+(\\.\\d+)?/g, \"<n>\")\n .replace(/([\"'`]).*?\\1/g, \"<str>\")\n .trim()\n .slice(0, 300);\n}\n\n/** Remove build hashes / absolute prefixes so the same code location matches across builds and devices. */\nexport function normaliseFile(file: string | undefined): string {\n if (!file) return \"\";\n // Metro dev bundles put options after \"//&\" instead of \"?\"; drop both.\n let f = file.replace(/\\/\\/&.*$/, \"\").replace(/[?#].*$/, \"\").replace(/^[a-z]+:\\/\\/[^/]+/i, \"\");\n // React Native bundles: the path is noise, the bundle name is enough.\n if (/\\.(?:js)?bundle$/.test(f)) return f.replace(/^.*\\//, \"\");\n // Keep the path from the deepest conventional source root, preferring src/.\n for (const root of [\"/src/\", \"/lib/\", \"/app/\"]) {\n const i = f.lastIndexOf(root);\n if (i >= 0) {\n f = f.slice(i + 1);\n break;\n }\n }\n return f.replace(/\\.[0-9a-f]{6,}\\./g, \".\");\n}\n\nexport function topInAppFrame(frames: StackFrame[] | undefined): StackFrame | undefined {\n return frames?.find((f) => f.inApp !== false && f.file) ?? frames?.[0];\n}\n\nexport function culpritOf(e: ErrorEvent): string | undefined {\n const f = topInAppFrame(e.error.frames);\n if (!f) return undefined;\n const loc = normaliseFile(f.file) + (f.line ? `:${f.line}` : \"\");\n return f.fn ? `${f.fn} (${loc})` : loc;\n}\n\n/**\n * Fingerprint = error name + top in-app frame (function + file, not line, so small edits\n * don't split issues) + flow. Falls back to the normalised message when there's no stack.\n */\nexport function fingerprint(e: ErrorEvent): string {\n const f = topInAppFrame(e.error.frames);\n // Failed requests group by endpoint and status class: every 503 from /v1/feed is one issue,\n // whichever screen made the call.\n const r = e.request;\n const where = r\n ? `${r.method.toUpperCase()} ${r.url} ${r.status ? `${Math.floor(r.status / 100)}xx` : \"failed\"}`\n : f\n ? `${f.fn ?? \"?\"}@${normaliseFile(f.file)}`\n : normaliseMessage(e.error.message);\n return createHash(\"sha256\")\n .update([e.error.name, where, e.flow ?? \"\"].join(\"|\"))\n .digest(\"hex\")\n .slice(0, 24);\n}\n\nexport function titleOf(e: ErrorEvent): string {\n return `${e.error.name}: ${e.error.message}`.slice(0, 200);\n}\n"]}
@@ -0,0 +1,25 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+
4
+ var _chunkYSIB36FScjs = require('./chunk-YSIB36FS.cjs');
5
+
6
+ // src/firebase.ts
7
+ function firebaseHandler(opts) {
8
+ return async (req, res) => {
9
+ res.set(_chunkYSIB36FScjs.CORS_HEADERS);
10
+ if (req.method === "OPTIONS") return res.status(204).send();
11
+ if (req.method !== "POST") return res.status(405).json({ ok: false, error: "method not allowed" });
12
+ const raw = _nullishCoalesce(_optionalChain([req, 'access', _ => _.rawBody, 'optionalAccess', _2 => _2.toString, 'call', _3 => _3("utf8")]), () => ( (typeof req.body === "string" ? req.body : JSON.stringify(_nullishCoalesce(req.body, () => ( {}))))));
13
+ try {
14
+ const out = await _chunkYSIB36FScjs.ingest.call(void 0, raw, req.header("x-debug-key"), opts);
15
+ res.status(out.status).json(out.body);
16
+ } catch (err) {
17
+ console.error("[deBug] ingest failed", err);
18
+ res.status(500).json({ ok: false, error: "internal" });
19
+ }
20
+ };
21
+ }
22
+
23
+
24
+ exports.firebaseHandler = firebaseHandler;
25
+ //# sourceMappingURL=firebase.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/sivemwanda/Work/Tanstack/deBug/packages/ingest/dist/firebase.cjs","../src/firebase.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACF,wDAA6B;AAC7B;AACA;ACYO,SAAS,eAAA,CAAgB,IAAA,EAAqB;AACnD,EAAA,OAAO,MAAA,CACL,GAAA,EACA,GAAA,EAAA,GACG;AACH,IAAA,GAAA,CAAI,GAAA,CAAI,8BAAY,CAAA;AACpB,IAAA,GAAA,CAAI,GAAA,CAAI,OAAA,IAAW,SAAA,EAAW,OAAO,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,CAAA;AAC1D,IAAA,GAAA,CAAI,GAAA,CAAI,OAAA,IAAW,MAAA,EAAQ,OAAO,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,qBAAqB,CAAC,CAAA;AACjG,IAAA,MAAM,IAAA,mCAAM,GAAA,mBAAI,OAAA,6BAAS,QAAA,mBAAS,MAAM,GAAA,UAAA,CAAM,OAAO,GAAA,CAAI,KAAA,IAAS,SAAA,EAAW,GAAA,CAAI,KAAA,EAAO,IAAA,CAAK,SAAA,kBAAU,GAAA,CAAI,IAAA,UAAQ,CAAC,GAAC,CAAA,GAAA;AACrH,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,EAAM,MAAM,sCAAA,GAAO,EAAK,GAAA,CAAI,MAAA,CAAO,aAAa,CAAA,EAAG,IAAI,CAAA;AAC7D,MAAA,GAAA,CAAI,MAAA,CAAO,GAAA,CAAI,MAAM,CAAA,CAAE,IAAA,CAAK,GAAA,CAAI,IAAI,CAAA;AAAA,IACtC,EAAA,MAAA,CAAS,GAAA,EAAK;AACZ,MAAA,OAAA,CAAQ,KAAA,CAAM,uBAAA,EAAyB,GAAG,CAAA;AAC1C,MAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,WAAW,CAAC,CAAA;AAAA,IACvD;AAAA,EACF,CAAA;AACF;ADbA;AACE;AACF,0CAAC","file":"/Users/sivemwanda/Work/Tanstack/deBug/packages/ingest/dist/firebase.cjs","sourcesContent":[null,"import { CORS_HEADERS, ingest, type IngestOptions } from \"./index\";\n\n/**\n * Express-style handler for Firebase Cloud Functions (v2):\n *\n * import { onRequest } from \"firebase-functions/v2/https\";\n * import { getFirestore } from \"firebase-admin/firestore\";\n * import { initializeApp } from \"firebase-admin/app\";\n * import { FirestoreStore } from \"@debugkit/adapters/firestore\";\n * import { firebaseHandler } from \"@debugkit/ingest/firebase\";\n * import { parseIngestKeys } from \"@debugkit/ingest\";\n *\n * initializeApp();\n * export const debugIngest = onRequest(\n * firebaseHandler({ store: new FirestoreStore(getFirestore()), keys: parseIngestKeys(process.env.DEBUG_INGEST_KEYS) }),\n * );\n */\nexport function firebaseHandler(opts: IngestOptions) {\n return async (\n req: { method: string; header(name: string): string | undefined; rawBody?: Buffer; body?: unknown },\n res: { set(h: Record<string, string>): unknown; status(n: number): { json(b: unknown): void; send(b?: unknown): void } },\n ) => {\n res.set(CORS_HEADERS);\n if (req.method === \"OPTIONS\") return res.status(204).send();\n if (req.method !== \"POST\") return res.status(405).json({ ok: false, error: \"method not allowed\" });\n const raw = req.rawBody?.toString(\"utf8\") ?? (typeof req.body === \"string\" ? req.body : JSON.stringify(req.body ?? {}));\n try {\n const out = await ingest(raw, req.header(\"x-debug-key\"), opts);\n res.status(out.status).json(out.body);\n } catch (err) {\n console.error(\"[deBug] ingest failed\", err);\n res.status(500).json({ ok: false, error: \"internal\" });\n }\n };\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import { IngestOptions } from './index.cjs';
2
+ import '@debugkit/adapters';
3
+ import '@debugkit/schema';
4
+
5
+ /**
6
+ * Express-style handler for Firebase Cloud Functions (v2):
7
+ *
8
+ * import { onRequest } from "firebase-functions/v2/https";
9
+ * import { getFirestore } from "firebase-admin/firestore";
10
+ * import { initializeApp } from "firebase-admin/app";
11
+ * import { FirestoreStore } from "@debugkit/adapters/firestore";
12
+ * import { firebaseHandler } from "@debugkit/ingest/firebase";
13
+ * import { parseIngestKeys } from "@debugkit/ingest";
14
+ *
15
+ * initializeApp();
16
+ * export const debugIngest = onRequest(
17
+ * firebaseHandler({ store: new FirestoreStore(getFirestore()), keys: parseIngestKeys(process.env.DEBUG_INGEST_KEYS) }),
18
+ * );
19
+ */
20
+ declare function firebaseHandler(opts: IngestOptions): (req: {
21
+ method: string;
22
+ header(name: string): string | undefined;
23
+ rawBody?: Buffer;
24
+ body?: unknown;
25
+ }, res: {
26
+ set(h: Record<string, string>): unknown;
27
+ status(n: number): {
28
+ json(b: unknown): void;
29
+ send(b?: unknown): void;
30
+ };
31
+ }) => Promise<void>;
32
+
33
+ export { firebaseHandler };
@@ -0,0 +1,33 @@
1
+ import { IngestOptions } from './index.js';
2
+ import '@debugkit/adapters';
3
+ import '@debugkit/schema';
4
+
5
+ /**
6
+ * Express-style handler for Firebase Cloud Functions (v2):
7
+ *
8
+ * import { onRequest } from "firebase-functions/v2/https";
9
+ * import { getFirestore } from "firebase-admin/firestore";
10
+ * import { initializeApp } from "firebase-admin/app";
11
+ * import { FirestoreStore } from "@debugkit/adapters/firestore";
12
+ * import { firebaseHandler } from "@debugkit/ingest/firebase";
13
+ * import { parseIngestKeys } from "@debugkit/ingest";
14
+ *
15
+ * initializeApp();
16
+ * export const debugIngest = onRequest(
17
+ * firebaseHandler({ store: new FirestoreStore(getFirestore()), keys: parseIngestKeys(process.env.DEBUG_INGEST_KEYS) }),
18
+ * );
19
+ */
20
+ declare function firebaseHandler(opts: IngestOptions): (req: {
21
+ method: string;
22
+ header(name: string): string | undefined;
23
+ rawBody?: Buffer;
24
+ body?: unknown;
25
+ }, res: {
26
+ set(h: Record<string, string>): unknown;
27
+ status(n: number): {
28
+ json(b: unknown): void;
29
+ send(b?: unknown): void;
30
+ };
31
+ }) => Promise<void>;
32
+
33
+ export { firebaseHandler };
@@ -0,0 +1,25 @@
1
+ import {
2
+ CORS_HEADERS,
3
+ ingest
4
+ } from "./chunk-6C67KV5S.js";
5
+
6
+ // src/firebase.ts
7
+ function firebaseHandler(opts) {
8
+ return async (req, res) => {
9
+ res.set(CORS_HEADERS);
10
+ if (req.method === "OPTIONS") return res.status(204).send();
11
+ if (req.method !== "POST") return res.status(405).json({ ok: false, error: "method not allowed" });
12
+ const raw = req.rawBody?.toString("utf8") ?? (typeof req.body === "string" ? req.body : JSON.stringify(req.body ?? {}));
13
+ try {
14
+ const out = await ingest(raw, req.header("x-debug-key"), opts);
15
+ res.status(out.status).json(out.body);
16
+ } catch (err) {
17
+ console.error("[deBug] ingest failed", err);
18
+ res.status(500).json({ ok: false, error: "internal" });
19
+ }
20
+ };
21
+ }
22
+ export {
23
+ firebaseHandler
24
+ };
25
+ //# sourceMappingURL=firebase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/firebase.ts"],"sourcesContent":["import { CORS_HEADERS, ingest, type IngestOptions } from \"./index\";\n\n/**\n * Express-style handler for Firebase Cloud Functions (v2):\n *\n * import { onRequest } from \"firebase-functions/v2/https\";\n * import { getFirestore } from \"firebase-admin/firestore\";\n * import { initializeApp } from \"firebase-admin/app\";\n * import { FirestoreStore } from \"@debugkit/adapters/firestore\";\n * import { firebaseHandler } from \"@debugkit/ingest/firebase\";\n * import { parseIngestKeys } from \"@debugkit/ingest\";\n *\n * initializeApp();\n * export const debugIngest = onRequest(\n * firebaseHandler({ store: new FirestoreStore(getFirestore()), keys: parseIngestKeys(process.env.DEBUG_INGEST_KEYS) }),\n * );\n */\nexport function firebaseHandler(opts: IngestOptions) {\n return async (\n req: { method: string; header(name: string): string | undefined; rawBody?: Buffer; body?: unknown },\n res: { set(h: Record<string, string>): unknown; status(n: number): { json(b: unknown): void; send(b?: unknown): void } },\n ) => {\n res.set(CORS_HEADERS);\n if (req.method === \"OPTIONS\") return res.status(204).send();\n if (req.method !== \"POST\") return res.status(405).json({ ok: false, error: \"method not allowed\" });\n const raw = req.rawBody?.toString(\"utf8\") ?? (typeof req.body === \"string\" ? req.body : JSON.stringify(req.body ?? {}));\n try {\n const out = await ingest(raw, req.header(\"x-debug-key\"), opts);\n res.status(out.status).json(out.body);\n } catch (err) {\n console.error(\"[deBug] ingest failed\", err);\n res.status(500).json({ ok: false, error: \"internal\" });\n }\n };\n}\n"],"mappings":";;;;;;AAiBO,SAAS,gBAAgB,MAAqB;AACnD,SAAO,OACL,KACA,QACG;AACH,QAAI,IAAI,YAAY;AACpB,QAAI,IAAI,WAAW,UAAW,QAAO,IAAI,OAAO,GAAG,EAAE,KAAK;AAC1D,QAAI,IAAI,WAAW,OAAQ,QAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,OAAO,OAAO,qBAAqB,CAAC;AACjG,UAAM,MAAM,IAAI,SAAS,SAAS,MAAM,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO,KAAK,UAAU,IAAI,QAAQ,CAAC,CAAC;AACrH,QAAI;AACF,YAAM,MAAM,MAAM,OAAO,KAAK,IAAI,OAAO,aAAa,GAAG,IAAI;AAC7D,UAAI,OAAO,IAAI,MAAM,EAAE,KAAK,IAAI,IAAI;AAAA,IACtC,SAAS,KAAK;AACZ,cAAQ,MAAM,yBAAyB,GAAG;AAC1C,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,OAAO,OAAO,WAAW,CAAC;AAAA,IACvD;AAAA,EACF;AACF;","names":[]}
package/dist/index.cjs ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+ var _chunkYSIB36FScjs = require('./chunk-YSIB36FS.cjs');
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+ exports.CORS_HEADERS = _chunkYSIB36FScjs.CORS_HEADERS; exports.culpritOf = _chunkYSIB36FScjs.culpritOf; exports.fingerprint = _chunkYSIB36FScjs.fingerprint; exports.ingest = _chunkYSIB36FScjs.ingest; exports.normaliseFile = _chunkYSIB36FScjs.normaliseFile; exports.normaliseMessage = _chunkYSIB36FScjs.normaliseMessage; exports.parseIngestKeys = _chunkYSIB36FScjs.parseIngestKeys; exports.titleOf = _chunkYSIB36FScjs.titleOf; exports.topInAppFrame = _chunkYSIB36FScjs.topInAppFrame;
23
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/sivemwanda/Work/Tanstack/deBug/packages/ingest/dist/index.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,keAAC","file":"/Users/sivemwanda/Work/Tanstack/deBug/packages/ingest/dist/index.cjs"}
@@ -0,0 +1,49 @@
1
+ import { WriteStore } from '@debugkit/adapters';
2
+ import { ErrorEvent, StackFrame } from '@debugkit/schema';
3
+
4
+ /** Strip volatile parts so "User 123 not found" and "User 456 not found" group together. */
5
+ declare function normaliseMessage(msg: string): string;
6
+ /** Remove build hashes / absolute prefixes so the same code location matches across builds and devices. */
7
+ declare function normaliseFile(file: string | undefined): string;
8
+ declare function topInAppFrame(frames: StackFrame[] | undefined): StackFrame | undefined;
9
+ declare function culpritOf(e: ErrorEvent): string | undefined;
10
+ /**
11
+ * Fingerprint = error name + top in-app frame (function + file, not line, so small edits
12
+ * don't split issues) + flow. Falls back to the normalised message when there's no stack.
13
+ */
14
+ declare function fingerprint(e: ErrorEvent): string;
15
+ declare function titleOf(e: ErrorEvent): string;
16
+
17
+ interface IngestOptions {
18
+ store: WriteStore;
19
+ /**
20
+ * Map of public ingest key → app id it may write to. A key can't write to another app.
21
+ * The app id "*" lets a key write to any app (local development only).
22
+ * Parse from env with `parseIngestKeys(process.env.DEBUG_INGEST_KEYS)`.
23
+ */
24
+ keys: Record<string, string>;
25
+ /** Hard cap on accepted payload size in bytes. Default 512 KB. */
26
+ maxBytes?: number;
27
+ now?: () => Date;
28
+ }
29
+ interface IngestResult {
30
+ status: number;
31
+ body: {
32
+ ok: boolean;
33
+ accepted?: number;
34
+ error?: string;
35
+ issues?: unknown;
36
+ };
37
+ }
38
+ /** "pk_abc:megaton-native,pk_def:other-app" → { pk_abc: "megaton-native", ... } */
39
+ declare function parseIngestKeys(raw: string | undefined): Record<string, string>;
40
+ /** Platform-agnostic ingest. Wrap with ./firebase or ./lambda to deploy. */
41
+ declare function ingest(rawBody: string, ingestKey: string | undefined, opts: IngestOptions): Promise<IngestResult>;
42
+ declare const CORS_HEADERS: {
43
+ "access-control-allow-origin": string;
44
+ "access-control-allow-methods": string;
45
+ "access-control-allow-headers": string;
46
+ "access-control-max-age": string;
47
+ };
48
+
49
+ export { CORS_HEADERS, type IngestOptions, type IngestResult, culpritOf, fingerprint, ingest, normaliseFile, normaliseMessage, parseIngestKeys, titleOf, topInAppFrame };
@@ -0,0 +1,49 @@
1
+ import { WriteStore } from '@debugkit/adapters';
2
+ import { ErrorEvent, StackFrame } from '@debugkit/schema';
3
+
4
+ /** Strip volatile parts so "User 123 not found" and "User 456 not found" group together. */
5
+ declare function normaliseMessage(msg: string): string;
6
+ /** Remove build hashes / absolute prefixes so the same code location matches across builds and devices. */
7
+ declare function normaliseFile(file: string | undefined): string;
8
+ declare function topInAppFrame(frames: StackFrame[] | undefined): StackFrame | undefined;
9
+ declare function culpritOf(e: ErrorEvent): string | undefined;
10
+ /**
11
+ * Fingerprint = error name + top in-app frame (function + file, not line, so small edits
12
+ * don't split issues) + flow. Falls back to the normalised message when there's no stack.
13
+ */
14
+ declare function fingerprint(e: ErrorEvent): string;
15
+ declare function titleOf(e: ErrorEvent): string;
16
+
17
+ interface IngestOptions {
18
+ store: WriteStore;
19
+ /**
20
+ * Map of public ingest key → app id it may write to. A key can't write to another app.
21
+ * The app id "*" lets a key write to any app (local development only).
22
+ * Parse from env with `parseIngestKeys(process.env.DEBUG_INGEST_KEYS)`.
23
+ */
24
+ keys: Record<string, string>;
25
+ /** Hard cap on accepted payload size in bytes. Default 512 KB. */
26
+ maxBytes?: number;
27
+ now?: () => Date;
28
+ }
29
+ interface IngestResult {
30
+ status: number;
31
+ body: {
32
+ ok: boolean;
33
+ accepted?: number;
34
+ error?: string;
35
+ issues?: unknown;
36
+ };
37
+ }
38
+ /** "pk_abc:megaton-native,pk_def:other-app" → { pk_abc: "megaton-native", ... } */
39
+ declare function parseIngestKeys(raw: string | undefined): Record<string, string>;
40
+ /** Platform-agnostic ingest. Wrap with ./firebase or ./lambda to deploy. */
41
+ declare function ingest(rawBody: string, ingestKey: string | undefined, opts: IngestOptions): Promise<IngestResult>;
42
+ declare const CORS_HEADERS: {
43
+ "access-control-allow-origin": string;
44
+ "access-control-allow-methods": string;
45
+ "access-control-allow-headers": string;
46
+ "access-control-max-age": string;
47
+ };
48
+
49
+ export { CORS_HEADERS, type IngestOptions, type IngestResult, culpritOf, fingerprint, ingest, normaliseFile, normaliseMessage, parseIngestKeys, titleOf, topInAppFrame };
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import {
2
+ CORS_HEADERS,
3
+ culpritOf,
4
+ fingerprint,
5
+ ingest,
6
+ normaliseFile,
7
+ normaliseMessage,
8
+ parseIngestKeys,
9
+ titleOf,
10
+ topInAppFrame
11
+ } from "./chunk-6C67KV5S.js";
12
+ export {
13
+ CORS_HEADERS,
14
+ culpritOf,
15
+ fingerprint,
16
+ ingest,
17
+ normaliseFile,
18
+ normaliseMessage,
19
+ parseIngestKeys,
20
+ titleOf,
21
+ topInAppFrame
22
+ };
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,31 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+
4
+ var _chunkYSIB36FScjs = require('./chunk-YSIB36FS.cjs');
5
+
6
+ // src/lambda.ts
7
+ function lambdaHandler(opts) {
8
+ return async (event) => {
9
+ const method = _nullishCoalesce(_nullishCoalesce(_optionalChain([event, 'access', _ => _.requestContext, 'optionalAccess', _2 => _2.http, 'optionalAccess', _3 => _3.method]), () => ( event.httpMethod)), () => ( "POST"));
10
+ if (method === "OPTIONS") return { statusCode: 204, headers: _chunkYSIB36FScjs.CORS_HEADERS, body: "" };
11
+ if (method !== "POST") return reply(405, { ok: false, error: "method not allowed" });
12
+ const headers = Object.fromEntries(Object.entries(_nullishCoalesce(event.headers, () => ( {}))).map(([k, v]) => [k.toLowerCase(), v]));
13
+ const raw = event.isBase64Encoded ? Buffer.from(_nullishCoalesce(event.body, () => ( "")), "base64").toString("utf8") : _nullishCoalesce(event.body, () => ( ""));
14
+ try {
15
+ const out = await _chunkYSIB36FScjs.ingest.call(void 0, raw, headers["x-debug-key"], opts);
16
+ return reply(out.status, out.body);
17
+ } catch (err) {
18
+ console.error("[deBug] ingest failed", err);
19
+ return reply(500, { ok: false, error: "internal" });
20
+ }
21
+ };
22
+ }
23
+ var reply = (statusCode, body) => ({
24
+ statusCode,
25
+ headers: { ..._chunkYSIB36FScjs.CORS_HEADERS, "content-type": "application/json" },
26
+ body: JSON.stringify(body)
27
+ });
28
+
29
+
30
+ exports.lambdaHandler = lambdaHandler;
31
+ //# sourceMappingURL=lambda.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/sivemwanda/Work/Tanstack/deBug/packages/ingest/dist/lambda.cjs","../src/lambda.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACF,wDAA6B;AAC7B;AACA;ACoBO,SAAS,aAAA,CAAc,IAAA,EAAqB;AACjD,EAAA,OAAO,MAAA,CAAO,KAAA,EAAA,GAAqB;AACjC,IAAA,MAAM,OAAA,oDAAS,KAAA,mBAAM,cAAA,6BAAgB,IAAA,6BAAM,QAAA,UAAU,KAAA,CAAM,YAAA,UAAc,QAAA;AACzE,IAAA,GAAA,CAAI,OAAA,IAAW,SAAA,EAAW,OAAO,EAAE,UAAA,EAAY,GAAA,EAAK,OAAA,EAAS,8BAAA,EAAc,IAAA,EAAM,GAAG,CAAA;AACpF,IAAA,GAAA,CAAI,OAAA,IAAW,MAAA,EAAQ,OAAO,KAAA,CAAM,GAAA,EAAK,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,qBAAqB,CAAC,CAAA;AACnF,IAAA,MAAM,QAAA,EAAU,MAAA,CAAO,WAAA,CAAY,MAAA,CAAO,OAAA,kBAAQ,KAAA,CAAM,OAAA,UAAW,CAAC,GAAC,CAAA,CAAE,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,CAAA,EAAA,GAAM,CAAC,CAAA,CAAE,WAAA,CAAY,CAAA,EAAG,CAAC,CAAC,CAAC,CAAA;AAC5G,IAAA,MAAM,IAAA,EAAM,KAAA,CAAM,gBAAA,EAAkB,MAAA,CAAO,IAAA,kBAAK,KAAA,CAAM,IAAA,UAAQ,IAAA,EAAI,QAAQ,CAAA,CAAE,QAAA,CAAS,MAAM,EAAA,mBAAK,KAAA,CAAM,IAAA,UAAQ,IAAA;AAC9G,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,EAAM,MAAM,sCAAA,GAAO,EAAK,OAAA,CAAQ,aAAa,CAAA,EAAG,IAAI,CAAA;AAC1D,MAAA,OAAO,KAAA,CAAM,GAAA,CAAI,MAAA,EAAQ,GAAA,CAAI,IAAI,CAAA;AAAA,IACnC,EAAA,MAAA,CAAS,GAAA,EAAK;AACZ,MAAA,OAAA,CAAQ,KAAA,CAAM,uBAAA,EAAyB,GAAG,CAAA;AAC1C,MAAA,OAAO,KAAA,CAAM,GAAA,EAAK,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,WAAW,CAAC,CAAA;AAAA,IACpD;AAAA,EACF,CAAA;AACF;AAEA,IAAM,MAAA,EAAQ,CAAC,UAAA,EAAoB,IAAA,EAAA,GAAA,CAAmB;AAAA,EACpD,UAAA;AAAA,EACA,OAAA,EAAS,EAAE,GAAG,8BAAA,EAAc,cAAA,EAAgB,mBAAmB,CAAA;AAAA,EAC/D,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,IAAI;AAC3B,CAAA,CAAA;ADnBA;AACE;AACF,sCAAC","file":"/Users/sivemwanda/Work/Tanstack/deBug/packages/ingest/dist/lambda.cjs","sourcesContent":[null,"import { CORS_HEADERS, ingest, type IngestOptions } from \"./index\";\n\ninterface HttpEvent {\n body?: string | null;\n isBase64Encoded?: boolean;\n headers?: Record<string, string | undefined>;\n requestContext?: { http?: { method?: string } };\n httpMethod?: string;\n}\n\n/**\n * Handler for AWS Lambda behind a Function URL or API Gateway (v1 or v2):\n *\n * import { DynamoDBClient } from \"@aws-sdk/client-dynamodb\";\n * import { DynamoDBDocumentClient } from \"@aws-sdk/lib-dynamodb\";\n * import { DynamoStore } from \"@debugkit/adapters/dynamodb\";\n * import { lambdaHandler } from \"@debugkit/ingest/lambda\";\n * import { parseIngestKeys } from \"@debugkit/ingest\";\n *\n * const doc = DynamoDBDocumentClient.from(new DynamoDBClient({}), { marshallOptions: { removeUndefinedValues: true } });\n * export const handler = lambdaHandler({\n * store: new DynamoStore(doc, { table: process.env.DEBUG_TABLE! }),\n * keys: parseIngestKeys(process.env.DEBUG_INGEST_KEYS),\n * });\n */\nexport function lambdaHandler(opts: IngestOptions) {\n return async (event: HttpEvent) => {\n const method = event.requestContext?.http?.method ?? event.httpMethod ?? \"POST\";\n if (method === \"OPTIONS\") return { statusCode: 204, headers: CORS_HEADERS, body: \"\" };\n if (method !== \"POST\") return reply(405, { ok: false, error: \"method not allowed\" });\n const headers = Object.fromEntries(Object.entries(event.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]));\n const raw = event.isBase64Encoded ? Buffer.from(event.body ?? \"\", \"base64\").toString(\"utf8\") : (event.body ?? \"\");\n try {\n const out = await ingest(raw, headers[\"x-debug-key\"], opts);\n return reply(out.status, out.body);\n } catch (err) {\n console.error(\"[deBug] ingest failed\", err);\n return reply(500, { ok: false, error: \"internal\" });\n }\n };\n}\n\nconst reply = (statusCode: number, body: unknown) => ({\n statusCode,\n headers: { ...CORS_HEADERS, \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n});\n"]}
@@ -0,0 +1,42 @@
1
+ import { IngestOptions } from './index.cjs';
2
+ import '@debugkit/adapters';
3
+ import '@debugkit/schema';
4
+
5
+ interface HttpEvent {
6
+ body?: string | null;
7
+ isBase64Encoded?: boolean;
8
+ headers?: Record<string, string | undefined>;
9
+ requestContext?: {
10
+ http?: {
11
+ method?: string;
12
+ };
13
+ };
14
+ httpMethod?: string;
15
+ }
16
+ /**
17
+ * Handler for AWS Lambda behind a Function URL or API Gateway (v1 or v2):
18
+ *
19
+ * import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
20
+ * import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
21
+ * import { DynamoStore } from "@debugkit/adapters/dynamodb";
22
+ * import { lambdaHandler } from "@debugkit/ingest/lambda";
23
+ * import { parseIngestKeys } from "@debugkit/ingest";
24
+ *
25
+ * const doc = DynamoDBDocumentClient.from(new DynamoDBClient({}), { marshallOptions: { removeUndefinedValues: true } });
26
+ * export const handler = lambdaHandler({
27
+ * store: new DynamoStore(doc, { table: process.env.DEBUG_TABLE! }),
28
+ * keys: parseIngestKeys(process.env.DEBUG_INGEST_KEYS),
29
+ * });
30
+ */
31
+ declare function lambdaHandler(opts: IngestOptions): (event: HttpEvent) => Promise<{
32
+ statusCode: number;
33
+ headers: {
34
+ "access-control-allow-origin": string;
35
+ "access-control-allow-methods": string;
36
+ "access-control-allow-headers": string;
37
+ "access-control-max-age": string;
38
+ };
39
+ body: string;
40
+ }>;
41
+
42
+ export { lambdaHandler };
@@ -0,0 +1,42 @@
1
+ import { IngestOptions } from './index.js';
2
+ import '@debugkit/adapters';
3
+ import '@debugkit/schema';
4
+
5
+ interface HttpEvent {
6
+ body?: string | null;
7
+ isBase64Encoded?: boolean;
8
+ headers?: Record<string, string | undefined>;
9
+ requestContext?: {
10
+ http?: {
11
+ method?: string;
12
+ };
13
+ };
14
+ httpMethod?: string;
15
+ }
16
+ /**
17
+ * Handler for AWS Lambda behind a Function URL or API Gateway (v1 or v2):
18
+ *
19
+ * import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
20
+ * import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
21
+ * import { DynamoStore } from "@debugkit/adapters/dynamodb";
22
+ * import { lambdaHandler } from "@debugkit/ingest/lambda";
23
+ * import { parseIngestKeys } from "@debugkit/ingest";
24
+ *
25
+ * const doc = DynamoDBDocumentClient.from(new DynamoDBClient({}), { marshallOptions: { removeUndefinedValues: true } });
26
+ * export const handler = lambdaHandler({
27
+ * store: new DynamoStore(doc, { table: process.env.DEBUG_TABLE! }),
28
+ * keys: parseIngestKeys(process.env.DEBUG_INGEST_KEYS),
29
+ * });
30
+ */
31
+ declare function lambdaHandler(opts: IngestOptions): (event: HttpEvent) => Promise<{
32
+ statusCode: number;
33
+ headers: {
34
+ "access-control-allow-origin": string;
35
+ "access-control-allow-methods": string;
36
+ "access-control-allow-headers": string;
37
+ "access-control-max-age": string;
38
+ };
39
+ body: string;
40
+ }>;
41
+
42
+ export { lambdaHandler };
package/dist/lambda.js ADDED
@@ -0,0 +1,31 @@
1
+ import {
2
+ CORS_HEADERS,
3
+ ingest
4
+ } from "./chunk-6C67KV5S.js";
5
+
6
+ // src/lambda.ts
7
+ function lambdaHandler(opts) {
8
+ return async (event) => {
9
+ const method = event.requestContext?.http?.method ?? event.httpMethod ?? "POST";
10
+ if (method === "OPTIONS") return { statusCode: 204, headers: CORS_HEADERS, body: "" };
11
+ if (method !== "POST") return reply(405, { ok: false, error: "method not allowed" });
12
+ const headers = Object.fromEntries(Object.entries(event.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]));
13
+ const raw = event.isBase64Encoded ? Buffer.from(event.body ?? "", "base64").toString("utf8") : event.body ?? "";
14
+ try {
15
+ const out = await ingest(raw, headers["x-debug-key"], opts);
16
+ return reply(out.status, out.body);
17
+ } catch (err) {
18
+ console.error("[deBug] ingest failed", err);
19
+ return reply(500, { ok: false, error: "internal" });
20
+ }
21
+ };
22
+ }
23
+ var reply = (statusCode, body) => ({
24
+ statusCode,
25
+ headers: { ...CORS_HEADERS, "content-type": "application/json" },
26
+ body: JSON.stringify(body)
27
+ });
28
+ export {
29
+ lambdaHandler
30
+ };
31
+ //# sourceMappingURL=lambda.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lambda.ts"],"sourcesContent":["import { CORS_HEADERS, ingest, type IngestOptions } from \"./index\";\n\ninterface HttpEvent {\n body?: string | null;\n isBase64Encoded?: boolean;\n headers?: Record<string, string | undefined>;\n requestContext?: { http?: { method?: string } };\n httpMethod?: string;\n}\n\n/**\n * Handler for AWS Lambda behind a Function URL or API Gateway (v1 or v2):\n *\n * import { DynamoDBClient } from \"@aws-sdk/client-dynamodb\";\n * import { DynamoDBDocumentClient } from \"@aws-sdk/lib-dynamodb\";\n * import { DynamoStore } from \"@debugkit/adapters/dynamodb\";\n * import { lambdaHandler } from \"@debugkit/ingest/lambda\";\n * import { parseIngestKeys } from \"@debugkit/ingest\";\n *\n * const doc = DynamoDBDocumentClient.from(new DynamoDBClient({}), { marshallOptions: { removeUndefinedValues: true } });\n * export const handler = lambdaHandler({\n * store: new DynamoStore(doc, { table: process.env.DEBUG_TABLE! }),\n * keys: parseIngestKeys(process.env.DEBUG_INGEST_KEYS),\n * });\n */\nexport function lambdaHandler(opts: IngestOptions) {\n return async (event: HttpEvent) => {\n const method = event.requestContext?.http?.method ?? event.httpMethod ?? \"POST\";\n if (method === \"OPTIONS\") return { statusCode: 204, headers: CORS_HEADERS, body: \"\" };\n if (method !== \"POST\") return reply(405, { ok: false, error: \"method not allowed\" });\n const headers = Object.fromEntries(Object.entries(event.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]));\n const raw = event.isBase64Encoded ? Buffer.from(event.body ?? \"\", \"base64\").toString(\"utf8\") : (event.body ?? \"\");\n try {\n const out = await ingest(raw, headers[\"x-debug-key\"], opts);\n return reply(out.status, out.body);\n } catch (err) {\n console.error(\"[deBug] ingest failed\", err);\n return reply(500, { ok: false, error: \"internal\" });\n }\n };\n}\n\nconst reply = (statusCode: number, body: unknown) => ({\n statusCode,\n headers: { ...CORS_HEADERS, \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n});\n"],"mappings":";;;;;;AAyBO,SAAS,cAAc,MAAqB;AACjD,SAAO,OAAO,UAAqB;AACjC,UAAM,SAAS,MAAM,gBAAgB,MAAM,UAAU,MAAM,cAAc;AACzE,QAAI,WAAW,UAAW,QAAO,EAAE,YAAY,KAAK,SAAS,cAAc,MAAM,GAAG;AACpF,QAAI,WAAW,OAAQ,QAAO,MAAM,KAAK,EAAE,IAAI,OAAO,OAAO,qBAAqB,CAAC;AACnF,UAAM,UAAU,OAAO,YAAY,OAAO,QAAQ,MAAM,WAAW,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,CAAC,CAAC,CAAC;AAC5G,UAAM,MAAM,MAAM,kBAAkB,OAAO,KAAK,MAAM,QAAQ,IAAI,QAAQ,EAAE,SAAS,MAAM,IAAK,MAAM,QAAQ;AAC9G,QAAI;AACF,YAAM,MAAM,MAAM,OAAO,KAAK,QAAQ,aAAa,GAAG,IAAI;AAC1D,aAAO,MAAM,IAAI,QAAQ,IAAI,IAAI;AAAA,IACnC,SAAS,KAAK;AACZ,cAAQ,MAAM,yBAAyB,GAAG;AAC1C,aAAO,MAAM,KAAK,EAAE,IAAI,OAAO,OAAO,WAAW,CAAC;AAAA,IACpD;AAAA,EACF;AACF;AAEA,IAAM,QAAQ,CAAC,YAAoB,UAAmB;AAAA,EACpD;AAAA,EACA,SAAS,EAAE,GAAG,cAAc,gBAAgB,mBAAmB;AAAA,EAC/D,MAAM,KAAK,UAAU,IAAI;AAC3B;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@debugkit/ingest",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": {
8
+ "import": "./dist/index.d.ts",
9
+ "require": "./dist/index.d.cts"
10
+ },
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ },
14
+ "./firebase": {
15
+ "types": {
16
+ "import": "./dist/firebase.d.ts",
17
+ "require": "./dist/firebase.d.cts"
18
+ },
19
+ "import": "./dist/firebase.js",
20
+ "require": "./dist/firebase.cjs"
21
+ },
22
+ "./lambda": {
23
+ "types": {
24
+ "import": "./dist/lambda.d.ts",
25
+ "require": "./dist/lambda.d.cts"
26
+ },
27
+ "import": "./dist/lambda.js",
28
+ "require": "./dist/lambda.cjs"
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "dependencies": {
33
+ "@debugkit/adapters": "^0.2.0",
34
+ "@debugkit/schema": "^0.2.0"
35
+ },
36
+ "devDependencies": {
37
+ "@debugkit/sdk": "^0.2.0"
38
+ },
39
+ "description": "deBug ingest: validate, group and store events. Deploy as a Firebase Cloud Function or AWS Lambda.",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/Abantu-Digital/deBug.git",
44
+ "directory": "packages/ingest"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md"
49
+ ],
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "typecheck": "tsc -p .",
55
+ "build": "tsup"
56
+ },
57
+ "main": "./dist/index.cjs",
58
+ "module": "./dist/index.js",
59
+ "types": "./dist/index.d.ts"
6
60
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.