@debugkit/ingest 0.1.0 → 0.3.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/dist/{chunk-MFFRC4YJ.js → chunk-6C67KV5S.js} +10 -4
- package/dist/chunk-6C67KV5S.js.map +1 -0
- package/dist/{chunk-KA26ALGE.cjs → chunk-YSIB36FS.cjs} +10 -4
- package/dist/chunk-YSIB36FS.cjs.map +1 -0
- package/dist/firebase.cjs +3 -3
- package/dist/firebase.js +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/dist/lambda.cjs +4 -4
- package/dist/lambda.js +1 -1
- package/package.json +4 -4
- package/dist/chunk-KA26ALGE.cjs.map +0 -1
- package/dist/chunk-MFFRC4YJ.js.map +0 -1
|
@@ -30,7 +30,8 @@ function culpritOf(e) {
|
|
|
30
30
|
}
|
|
31
31
|
function fingerprint(e) {
|
|
32
32
|
const f = topInAppFrame(e.error.frames);
|
|
33
|
-
const
|
|
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);
|
|
34
35
|
return createHash("sha256").update([e.error.name, where, e.flow ?? ""].join("|")).digest("hex").slice(0, 24);
|
|
35
36
|
}
|
|
36
37
|
function titleOf(e) {
|
|
@@ -84,6 +85,10 @@ async function ingest(rawBody, ingestKey, opts) {
|
|
|
84
85
|
const event = { ...raw, ts: sanitiseTs(raw.ts, now) };
|
|
85
86
|
if (event.type === "error" && !event.error.frames?.length && event.error.stack) event.error.frames = parseStack(event.error.stack);
|
|
86
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
|
+
}
|
|
87
92
|
const kind = event.type === "error" ? "error" : "event";
|
|
88
93
|
const inc = (dimension, key) => rollups.push({ app, day, dimension, key: key || "unknown", kind, by: 1 });
|
|
89
94
|
let fp;
|
|
@@ -103,6 +108,7 @@ async function ingest(rawBody, ingestKey, opts) {
|
|
|
103
108
|
lastEvent: cur && cur.ts > event.ts ? cur.lastEvent : se
|
|
104
109
|
});
|
|
105
110
|
inc("issue", fp);
|
|
111
|
+
if (event.request) inc("endpoint", `${event.request.method.toUpperCase()} ${event.request.url}`);
|
|
106
112
|
rollups.push({ app, day: event.ts.slice(0, 13), dimension: "issueHour", key: fp, kind, by: 1 });
|
|
107
113
|
}
|
|
108
114
|
inc("total", "all");
|
|
@@ -115,11 +121,11 @@ async function ingest(rawBody, ingestKey, opts) {
|
|
|
115
121
|
}
|
|
116
122
|
await opts.store.registerApp(app);
|
|
117
123
|
await Promise.all([
|
|
118
|
-
opts.store.writeEvents(stored),
|
|
124
|
+
stored.length ? opts.store.writeEvents(stored) : void 0,
|
|
119
125
|
opts.store.upsertIssues([...issues.values()]),
|
|
120
126
|
opts.store.incrementRollups(rollups)
|
|
121
127
|
]);
|
|
122
|
-
return { status: 202, body: { ok: true, accepted:
|
|
128
|
+
return { status: 202, body: { ok: true, accepted: env.events.length } };
|
|
123
129
|
}
|
|
124
130
|
var CORS_HEADERS = {
|
|
125
131
|
"access-control-allow-origin": "*",
|
|
@@ -139,4 +145,4 @@ export {
|
|
|
139
145
|
ingest,
|
|
140
146
|
CORS_HEADERS
|
|
141
147
|
};
|
|
142
|
-
//# sourceMappingURL=chunk-
|
|
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":[]}
|
|
@@ -30,7 +30,8 @@ function culpritOf(e) {
|
|
|
30
30
|
}
|
|
31
31
|
function fingerprint(e) {
|
|
32
32
|
const f = topInAppFrame(e.error.frames);
|
|
33
|
-
const
|
|
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);
|
|
34
35
|
return _crypto.createHash.call(void 0, "sha256").update([e.error.name, where, _nullishCoalesce(e.flow, () => ( ""))].join("|")).digest("hex").slice(0, 24);
|
|
35
36
|
}
|
|
36
37
|
function titleOf(e) {
|
|
@@ -84,6 +85,10 @@ async function ingest(rawBody, ingestKey, opts) {
|
|
|
84
85
|
const event = { ...raw, ts: sanitiseTs(raw.ts, now) };
|
|
85
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);
|
|
86
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
|
+
}
|
|
87
92
|
const kind = event.type === "error" ? "error" : "event";
|
|
88
93
|
const inc = (dimension, key) => rollups.push({ app, day, dimension, key: key || "unknown", kind, by: 1 });
|
|
89
94
|
let fp;
|
|
@@ -103,6 +108,7 @@ async function ingest(rawBody, ingestKey, opts) {
|
|
|
103
108
|
lastEvent: cur && cur.ts > event.ts ? cur.lastEvent : se
|
|
104
109
|
});
|
|
105
110
|
inc("issue", fp);
|
|
111
|
+
if (event.request) inc("endpoint", `${event.request.method.toUpperCase()} ${event.request.url}`);
|
|
106
112
|
rollups.push({ app, day: event.ts.slice(0, 13), dimension: "issueHour", key: fp, kind, by: 1 });
|
|
107
113
|
}
|
|
108
114
|
inc("total", "all");
|
|
@@ -115,11 +121,11 @@ async function ingest(rawBody, ingestKey, opts) {
|
|
|
115
121
|
}
|
|
116
122
|
await opts.store.registerApp(app);
|
|
117
123
|
await Promise.all([
|
|
118
|
-
opts.store.writeEvents(stored),
|
|
124
|
+
stored.length ? opts.store.writeEvents(stored) : void 0,
|
|
119
125
|
opts.store.upsertIssues([...issues.values()]),
|
|
120
126
|
opts.store.incrementRollups(rollups)
|
|
121
127
|
]);
|
|
122
|
-
return { status: 202, body: { ok: true, accepted:
|
|
128
|
+
return { status: 202, body: { ok: true, accepted: env.events.length } };
|
|
123
129
|
}
|
|
124
130
|
var CORS_HEADERS = {
|
|
125
131
|
"access-control-allow-origin": "*",
|
|
@@ -139,4 +145,4 @@ var CORS_HEADERS = {
|
|
|
139
145
|
|
|
140
146
|
|
|
141
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;
|
|
142
|
-
//# sourceMappingURL=chunk-
|
|
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"]}
|
package/dist/firebase.cjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
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
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkYSIB36FScjs = require('./chunk-YSIB36FS.cjs');
|
|
5
5
|
|
|
6
6
|
// src/firebase.ts
|
|
7
7
|
function firebaseHandler(opts) {
|
|
8
8
|
return async (req, res) => {
|
|
9
|
-
res.set(
|
|
9
|
+
res.set(_chunkYSIB36FScjs.CORS_HEADERS);
|
|
10
10
|
if (req.method === "OPTIONS") return res.status(204).send();
|
|
11
11
|
if (req.method !== "POST") return res.status(405).json({ ok: false, error: "method not allowed" });
|
|
12
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
13
|
try {
|
|
14
|
-
const out = await
|
|
14
|
+
const out = await _chunkYSIB36FScjs.ingest.call(void 0, raw, req.header("x-debug-key"), opts);
|
|
15
15
|
res.status(out.status).json(out.body);
|
|
16
16
|
} catch (err) {
|
|
17
17
|
console.error("[deBug] ingest failed", err);
|
package/dist/firebase.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
var
|
|
11
|
+
var _chunkYSIB36FScjs = require('./chunk-YSIB36FS.cjs');
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
@@ -19,5 +19,5 @@ var _chunkKA26ALGEcjs = require('./chunk-KA26ALGE.cjs');
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
exports.CORS_HEADERS =
|
|
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
23
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.js
CHANGED
package/dist/lambda.cjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
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
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkYSIB36FScjs = require('./chunk-YSIB36FS.cjs');
|
|
5
5
|
|
|
6
6
|
// src/lambda.ts
|
|
7
7
|
function lambdaHandler(opts) {
|
|
8
8
|
return async (event) => {
|
|
9
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:
|
|
10
|
+
if (method === "OPTIONS") return { statusCode: 204, headers: _chunkYSIB36FScjs.CORS_HEADERS, body: "" };
|
|
11
11
|
if (method !== "POST") return reply(405, { ok: false, error: "method not allowed" });
|
|
12
12
|
const headers = Object.fromEntries(Object.entries(_nullishCoalesce(event.headers, () => ( {}))).map(([k, v]) => [k.toLowerCase(), v]));
|
|
13
13
|
const raw = event.isBase64Encoded ? Buffer.from(_nullishCoalesce(event.body, () => ( "")), "base64").toString("utf8") : _nullishCoalesce(event.body, () => ( ""));
|
|
14
14
|
try {
|
|
15
|
-
const out = await
|
|
15
|
+
const out = await _chunkYSIB36FScjs.ingest.call(void 0, raw, headers["x-debug-key"], opts);
|
|
16
16
|
return reply(out.status, out.body);
|
|
17
17
|
} catch (err) {
|
|
18
18
|
console.error("[deBug] ingest failed", err);
|
|
@@ -22,7 +22,7 @@ function lambdaHandler(opts) {
|
|
|
22
22
|
}
|
|
23
23
|
var reply = (statusCode, body) => ({
|
|
24
24
|
statusCode,
|
|
25
|
-
headers: { ...
|
|
25
|
+
headers: { ..._chunkYSIB36FScjs.CORS_HEADERS, "content-type": "application/json" },
|
|
26
26
|
body: JSON.stringify(body)
|
|
27
27
|
});
|
|
28
28
|
|
package/dist/lambda.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@debugkit/ingest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"./package.json": "./package.json"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@debugkit/
|
|
34
|
-
"@debugkit/
|
|
33
|
+
"@debugkit/adapters": "^0.3.0",
|
|
34
|
+
"@debugkit/schema": "^0.3.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@debugkit/sdk": "^0.
|
|
37
|
+
"@debugkit/sdk": "^0.3.0"
|
|
38
38
|
},
|
|
39
39
|
"description": "deBug ingest: validate, group and store events. Deploy as a Firebase Cloud Function or AWS Lambda.",
|
|
40
40
|
"license": "MIT",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/sivemwanda/Work/Tanstack/deBug/packages/ingest/dist/chunk-KA26ALGE.cjs","../src/index.ts","../src/fingerprint.ts"],"names":[],"mappings":"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","file":"/Users/sivemwanda/Work/Tanstack/deBug/packages/ingest/dist/chunk-KA26ALGE.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 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 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 opts.store.writeEvents(stored),\n opts.store.upsertIssues([...issues.values()]),\n opts.store.incrementRollups(rollups),\n ]);\n\n return { status: 202, body: { ok: true, accepted: stored.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 const where = f ? `${f.fn ?? \"?\"}@${normaliseFile(f.file)}` : 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"]}
|
|
@@ -1 +0,0 @@
|
|
|
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 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 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 opts.store.writeEvents(stored),\n opts.store.upsertIssues([...issues.values()]),\n opts.store.incrementRollups(rollups),\n ]);\n\n return { status: 202, body: { ok: true, accepted: stored.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 const where = f ? `${f.fn ?? \"?\"}@${normaliseFile(f.file)}` : 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":[]}
|