@autter/otlp-ingester 1.4.0 → 1.6.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.
Files changed (52) hide show
  1. package/README.md +51 -0
  2. package/dist/breadcrumbs.d.ts +41 -0
  3. package/dist/breadcrumbs.js +151 -0
  4. package/dist/clickhouse.d.ts +42 -5
  5. package/dist/clickhouse.js +168 -7
  6. package/dist/compat-manifest.json +123 -0
  7. package/dist/compat.d.ts +149 -0
  8. package/dist/compat.js +221 -0
  9. package/dist/config.d.ts +23 -0
  10. package/dist/config.js +19 -0
  11. package/dist/context.js +24 -11
  12. package/dist/error-fields.d.ts +41 -0
  13. package/dist/error-fields.js +103 -0
  14. package/dist/fingerprint-override.d.ts +42 -0
  15. package/dist/fingerprint-override.js +76 -0
  16. package/dist/fingerprint-v3.d.ts +54 -0
  17. package/dist/fingerprint-v3.js +96 -0
  18. package/dist/fingerprint.d.ts +54 -0
  19. package/dist/fingerprint.js +46 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +5 -3
  22. package/dist/llm.js +16 -3
  23. package/dist/log-promotion.d.ts +55 -0
  24. package/dist/log-promotion.js +119 -0
  25. package/dist/logs.d.ts +57 -1
  26. package/dist/logs.js +148 -11
  27. package/dist/memory.d.ts +2 -2
  28. package/dist/migrations.d.ts +13 -0
  29. package/dist/migrations.js +75 -2
  30. package/dist/normalize-browser.d.ts +67 -22
  31. package/dist/normalize-browser.js +46 -19
  32. package/dist/normalize-otlp.js +18 -2
  33. package/dist/redact.d.ts +33 -0
  34. package/dist/redact.js +96 -0
  35. package/dist/sdk-versions.d.ts +58 -0
  36. package/dist/sdk-versions.js +167 -0
  37. package/dist/server.d.ts +3 -0
  38. package/dist/server.js +219 -15
  39. package/dist/sink.d.ts +13 -0
  40. package/dist/sink.js +37 -4
  41. package/dist/source-maps-lookup.d.ts +31 -0
  42. package/dist/source-maps-lookup.js +64 -0
  43. package/dist/source-maps.d.ts +63 -2
  44. package/dist/source-maps.js +121 -13
  45. package/dist/stack-fixtures.d.ts +10 -2
  46. package/dist/stack-fixtures.js +13 -3
  47. package/dist/stack-frames.d.ts +48 -0
  48. package/dist/stack-frames.js +441 -0
  49. package/dist/symbolicate.d.ts +79 -0
  50. package/dist/symbolicate.js +257 -0
  51. package/dist/types.d.ts +86 -0
  52. package/package.json +2 -1
@@ -0,0 +1,123 @@
1
+ {
2
+ "manifestVersion": 1,
3
+ "docs": "https://github.com/Autter-dev/autter-runtime/blob/main/docs/COMPATIBILITY.md",
4
+ "upgrade": {
5
+ "ingester": "docker pull ghcr.io/autter-dev/otlp-ingester:latest and restart it (ClickHouse migrations run at boot)",
6
+ "sdk": "npm install {package}@latest"
7
+ },
8
+ "features": [
9
+ {
10
+ "id": "llm_calls",
11
+ "title": "LLM call tracking",
12
+ "ingester": "1.1.0",
13
+ "migrations": ["0004-llm-calls"],
14
+ "sdks": { "@autter/runtime-node": "1.1.0", "@autter/runtime-next": "1.1.0" }
15
+ },
16
+ {
17
+ "id": "endpoint_latency",
18
+ "title": "Endpoint latency regression detection",
19
+ "ingester": "1.3.1",
20
+ "migrations": ["0005-latency-histograms"],
21
+ "sdks": { "@autter/runtime-node": "1.3.0", "@autter/runtime-next": "1.3.0" }
22
+ },
23
+ {
24
+ "id": "browser_network_events",
25
+ "title": "Browser network, timing and outcome capture",
26
+ "ingester": "1.3.2",
27
+ "migrations": [],
28
+ "sdks": { "@autter/runtime-browser": "1.3.2", "@autter/runtime-next": "1.3.2" },
29
+ "browserEvents": ["request_failure", "timing", "outcome"]
30
+ },
31
+ {
32
+ "id": "profiles",
33
+ "title": "Profile ingestion",
34
+ "ingester": "1.3.2",
35
+ "migrations": ["0006-profile-samples"],
36
+ "route": "/v1/profiles",
37
+ "sdks": {}
38
+ },
39
+ {
40
+ "id": "source_maps",
41
+ "title": "Source map upload",
42
+ "ingester": "1.3.2",
43
+ "migrations": ["0007-source-maps"],
44
+ "route": "/v1/sourcemaps",
45
+ "sdks": {}
46
+ },
47
+ {
48
+ "id": "memory_metrics",
49
+ "title": "Memory pressure detection",
50
+ "ingester": "1.3.3",
51
+ "migrations": ["0008-memory-signals", "0010-memory-temporality"],
52
+ "sdks": { "@autter/runtime-node": "1.3.3", "@autter/runtime-next": "1.3.3" }
53
+ },
54
+ {
55
+ "id": "platform_events",
56
+ "title": "Platform OOM/restart events",
57
+ "ingester": "1.3.3",
58
+ "migrations": ["0008-memory-signals"],
59
+ "route": "/v1/platform-events",
60
+ "sdks": {}
61
+ },
62
+ {
63
+ "id": "csp_violations",
64
+ "title": "Browser CSP violation capture",
65
+ "ingester": "1.3.4",
66
+ "migrations": [],
67
+ "sdks": { "@autter/runtime-browser": "1.3.3", "@autter/runtime-next": "1.3.4" },
68
+ "browserEvents": ["csp_violation"]
69
+ },
70
+ {
71
+ "id": "operation_logging",
72
+ "title": "Operation logging",
73
+ "ingester": "1.4.0",
74
+ "migrations": ["0011-runtime-logs"],
75
+ "route": "/v1/logs",
76
+ "sdks": { "@autter/runtime-node": "1.4.0", "@autter/runtime-next": "1.4.0" }
77
+ },
78
+ {
79
+ "id": "request_summaries",
80
+ "title": "Request summaries and the per-route request rollup",
81
+ "ingester": "1.5.0",
82
+ "migrations": ["0012-runtime-logs-requests", "0014-runtime-request-1m"],
83
+ "route": "/v1/logs",
84
+ "sdks": { "@autter/runtime-node": "1.5.0", "@autter/runtime-next": "1.5.0", "@autter/runtime-edge": "1.0.0" }
85
+ },
86
+ {
87
+ "id": "coded_errors",
88
+ "title": "Coded errors and code-based grouping",
89
+ "ingester": "1.5.0",
90
+ "migrations": ["0013-occurrence-codes"],
91
+ "sdks": { "@autter/runtime-node": "1.5.0", "@autter/runtime-next": "1.5.0", "@autter/runtime-browser": "1.4.0", "@autter/runtime-edge": "1.0.0" }
92
+ },
93
+ {
94
+ "id": "breadcrumbs",
95
+ "title": "Breadcrumbs: the steps before an error",
96
+ "ingester": "1.6.0",
97
+ "migrations": [],
98
+ "sdks": { "@autter/runtime-browser": "1.6.0", "@autter/runtime-node": "1.6.0", "@autter/runtime-next": "1.6.0" }
99
+ },
100
+ {
101
+ "id": "source_map_debug_ids",
102
+ "title": "Source map debug ids",
103
+ "ingester": "1.6.0",
104
+ "migrations": ["0017-source-maps-debug-id"],
105
+ "route": "/v1/sourcemaps/check",
106
+ "sdks": { "@autter/runtime-browser": "1.6.0", "@autter/runtime-next": "1.6.0" }
107
+ },
108
+ {
109
+ "id": "fingerprint_override",
110
+ "title": "Fingerprint override",
111
+ "ingester": "1.6.0",
112
+ "migrations": [],
113
+ "sdks": { "@autter/runtime-browser": "1.6.0", "@autter/runtime-node": "1.6.0", "@autter/runtime-next": "1.6.0", "@autter/runtime-edge": "1.1.0" }
114
+ },
115
+ {
116
+ "id": "browser_trace_propagation",
117
+ "title": "Browser trace propagation (browser errors linked to server traces)",
118
+ "ingester": "1.6.0",
119
+ "migrations": [],
120
+ "sdks": { "@autter/runtime-browser": "1.6.0", "@autter/runtime-next": "1.6.0", "@autter/runtime-node": "1.6.0" }
121
+ }
122
+ ]
123
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Version compatibility between Autter Runtime SDKs, this ingester and its
3
+ * ClickHouse schema — the single source of truth.
4
+ *
5
+ * `compat-manifest.json` lists every feature that needs a minimum ingester
6
+ * version, a ClickHouse migration, a dedicated ingest route, or a minimum SDK
7
+ * version. Everything else derives from it:
8
+ *
9
+ * - the ingester serves it (evaluated against its own version and applied
10
+ * migrations) at `GET /v1/compat`;
11
+ * - `@autter/runtime-node` / `@autter/runtime-next` bundle this file at build
12
+ * time to warn once when an enabled feature needs a newer ingester, and to
13
+ * power `npx @autter/runtime-node doctor`;
14
+ * - the Python adapter (`adapters/python/compat.py`) embeds a copy that a
15
+ * parity test keeps identical.
16
+ *
17
+ * This module is dependency-free and side-effect-free on purpose: it is
18
+ * bundled into the Node SDK. Keep it that way.
19
+ *
20
+ * Adding a feature: append an entry to compat-manifest.json with the first
21
+ * ingester release that handles it, the migrations it writes to, its route
22
+ * (if it has its own), and the first SDK release that emits it. The
23
+ * consistency test in compat.test.ts checks ids, migrations and routes, and
24
+ * that no version is ahead of its package's package.json: a feature for the
25
+ * next release bumps the packages it names in the same change.
26
+ */
27
+ export interface CompatFeature {
28
+ id: string;
29
+ title: string;
30
+ /** First ingester release that stores this feature correctly. */
31
+ ingester: string;
32
+ /** ClickHouse migrations (migrations.ts ids) the feature writes to. */
33
+ migrations: string[];
34
+ /** Dedicated ingest route, when the feature has one. */
35
+ route?: string;
36
+ /** First release of each SDK package that emits the feature. */
37
+ sdks: Record<string, string>;
38
+ /** Browser payload event types that belong to this feature. */
39
+ browserEvents?: string[];
40
+ }
41
+ export interface CompatManifest {
42
+ manifestVersion: number;
43
+ docs: string;
44
+ upgrade: {
45
+ ingester: string;
46
+ sdk: string;
47
+ };
48
+ features: CompatFeature[];
49
+ }
50
+ export declare const COMPAT_MANIFEST: CompatManifest;
51
+ export declare const INGESTER_PACKAGE = "@autter/otlp-ingester";
52
+ /** Response header carrying the ingester version on every ingest response. */
53
+ export declare const INGESTER_VERSION_HEADER = "x-autter-ingester-version";
54
+ /** Semver-ish comparison; null when either side is unparsable. */
55
+ export declare function compareVersions(a: string, b: string): number | null;
56
+ /** True/false when comparable, null when the version is unknown/unparsable. */
57
+ export declare function versionAtLeast(version: string | null | undefined, minimum: string): boolean | null;
58
+ export type SchemaStatus = "ready" | "pending" | "failed" | "unconfigured";
59
+ /** What is known about the ingester an SDK talks to. */
60
+ export interface IngesterCompatInfo {
61
+ /** Exact version, or null when the ingester predates /v1/compat. */
62
+ version: string | null;
63
+ /** True when /v1/compat returned 404 (ingester 1.4.0 or older). */
64
+ legacy?: boolean;
65
+ schema?: {
66
+ status: SchemaStatus;
67
+ applied: string[];
68
+ };
69
+ /** Legacy ingesters: route → exists, learned by probing. */
70
+ routes?: Record<string, boolean>;
71
+ }
72
+ export type CompatIssueKind = "ingester_too_old" | "schema_not_applied" | "sdk_too_old";
73
+ export interface CompatIssue {
74
+ feature: string;
75
+ title: string;
76
+ kind: CompatIssueKind;
77
+ /** Package that must change: the ingester or an SDK package name. */
78
+ component: string;
79
+ required: string;
80
+ /** What was found ("1.3.4", "1.4.0 or older", …). */
81
+ actual: string;
82
+ /** One line: what is wrong, versions, and the fix. */
83
+ message: string;
84
+ fix: string;
85
+ }
86
+ export interface SdkIdentity {
87
+ name: string;
88
+ version: string;
89
+ }
90
+ export declare function featureById(id: string, manifest?: CompatManifest): CompatFeature | undefined;
91
+ /** Features the given browser payload event types belong to. */
92
+ export declare function featuresForBrowserEvents(types: Iterable<string>, manifest?: CompatManifest): string[];
93
+ export declare function ingesterUpgradeHint(manifest?: CompatManifest): string;
94
+ /**
95
+ * Every incompatibility for the features in use. Unknown is never an issue:
96
+ * when the ingester version can't be determined (and a legacy route probe
97
+ * didn't prove a route missing) nothing is reported, so the check has no
98
+ * false positives.
99
+ */
100
+ export declare function evaluateCompat(input: {
101
+ features: Iterable<string>;
102
+ ingester: IngesterCompatInfo | null;
103
+ sdk?: SdkIdentity | null;
104
+ manifest?: CompatManifest;
105
+ }): CompatIssue[];
106
+ export interface CompatFeatureStatus {
107
+ id: string;
108
+ title: string;
109
+ ingester: string;
110
+ migrations: string[];
111
+ route?: string;
112
+ sdks: Record<string, string>;
113
+ browserEvents?: string[];
114
+ /** This ingester's version is new enough. */
115
+ ingesterSupported: boolean;
116
+ /** Migrations the feature needs that are not applied yet. */
117
+ missingMigrations: string[];
118
+ /** Ingester new enough AND schema ready with every migration applied. */
119
+ available: boolean;
120
+ }
121
+ export interface CompatReport {
122
+ ingester: {
123
+ name: string;
124
+ version: string;
125
+ };
126
+ schema: {
127
+ status: SchemaStatus;
128
+ /** Highest applied migration id ("" when none known). */
129
+ level: string;
130
+ /** Highest migration this ingester ships. */
131
+ latest: string;
132
+ applied: string[];
133
+ };
134
+ features: CompatFeatureStatus[];
135
+ manifestVersion: number;
136
+ docs: string;
137
+ upgrade: CompatManifest["upgrade"];
138
+ /** Present when the request named features (and optionally an SDK). */
139
+ issues?: CompatIssue[];
140
+ }
141
+ export declare function buildCompatReport(input: {
142
+ version: string;
143
+ schemaStatus: SchemaStatus;
144
+ applied: string[];
145
+ allMigrations: string[];
146
+ manifest?: CompatManifest;
147
+ }): CompatReport;
148
+ /** IngesterCompatInfo from a /v1/compat response body (defensive: it's network input). */
149
+ export declare function ingesterInfoFromReport(body: unknown): IngesterCompatInfo | null;
package/dist/compat.js ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Version compatibility between Autter Runtime SDKs, this ingester and its
3
+ * ClickHouse schema — the single source of truth.
4
+ *
5
+ * `compat-manifest.json` lists every feature that needs a minimum ingester
6
+ * version, a ClickHouse migration, a dedicated ingest route, or a minimum SDK
7
+ * version. Everything else derives from it:
8
+ *
9
+ * - the ingester serves it (evaluated against its own version and applied
10
+ * migrations) at `GET /v1/compat`;
11
+ * - `@autter/runtime-node` / `@autter/runtime-next` bundle this file at build
12
+ * time to warn once when an enabled feature needs a newer ingester, and to
13
+ * power `npx @autter/runtime-node doctor`;
14
+ * - the Python adapter (`adapters/python/compat.py`) embeds a copy that a
15
+ * parity test keeps identical.
16
+ *
17
+ * This module is dependency-free and side-effect-free on purpose: it is
18
+ * bundled into the Node SDK. Keep it that way.
19
+ *
20
+ * Adding a feature: append an entry to compat-manifest.json with the first
21
+ * ingester release that handles it, the migrations it writes to, its route
22
+ * (if it has its own), and the first SDK release that emits it. The
23
+ * consistency test in compat.test.ts checks ids, migrations and routes, and
24
+ * that no version is ahead of its package's package.json: a feature for the
25
+ * next release bumps the packages it names in the same change.
26
+ */
27
+ import manifestJson from "./compat-manifest.json" with { type: "json" };
28
+ export const COMPAT_MANIFEST = manifestJson;
29
+ export const INGESTER_PACKAGE = "@autter/otlp-ingester";
30
+ /** Response header carrying the ingester version on every ingest response. */
31
+ export const INGESTER_VERSION_HEADER = "x-autter-ingester-version";
32
+ // ── versions ────────────────────────────────────────────────────────────
33
+ /** [major, minor, patch, isPrerelease] or null when unparsable. */
34
+ function parseVersion(version) {
35
+ if (typeof version !== "string")
36
+ return null;
37
+ const match = /^v?(\d{1,6})\.(\d{1,6})\.(\d{1,6})(-[0-9A-Za-z.-]+)?/.exec(version.trim());
38
+ if (!match)
39
+ return null;
40
+ return [
41
+ Number(match[1]),
42
+ Number(match[2]),
43
+ Number(match[3]),
44
+ // A prerelease sorts before its release: 1.4.0-rc.1 < 1.4.0.
45
+ match[4] ? 0 : 1,
46
+ ];
47
+ }
48
+ /** Semver-ish comparison; null when either side is unparsable. */
49
+ export function compareVersions(a, b) {
50
+ const left = parseVersion(a);
51
+ const right = parseVersion(b);
52
+ if (!left || !right)
53
+ return null;
54
+ for (let i = 0; i < 4; i++) {
55
+ if (left[i] !== right[i])
56
+ return left[i] < right[i] ? -1 : 1;
57
+ }
58
+ return 0;
59
+ }
60
+ /** True/false when comparable, null when the version is unknown/unparsable. */
61
+ export function versionAtLeast(version, minimum) {
62
+ if (!version)
63
+ return null;
64
+ const result = compareVersions(version, minimum);
65
+ return result === null ? null : result >= 0;
66
+ }
67
+ export function featureById(id, manifest = COMPAT_MANIFEST) {
68
+ return manifest.features.find((feature) => feature.id === id);
69
+ }
70
+ /** Features the given browser payload event types belong to. */
71
+ export function featuresForBrowserEvents(types, manifest = COMPAT_MANIFEST) {
72
+ const wanted = new Set(types);
73
+ return manifest.features
74
+ .filter((feature) => feature.browserEvents?.some((type) => wanted.has(type)))
75
+ .map((feature) => feature.id);
76
+ }
77
+ export function ingesterUpgradeHint(manifest = COMPAT_MANIFEST) {
78
+ return `Upgrade the ingester: ${manifest.upgrade.ingester}. See ${manifest.docs}`;
79
+ }
80
+ function sdkUpgradeHint(pkg, manifest) {
81
+ return `Upgrade the SDK: ${manifest.upgrade.sdk.replace("{package}", pkg)}. See ${manifest.docs}`;
82
+ }
83
+ /**
84
+ * Every incompatibility for the features in use. Unknown is never an issue:
85
+ * when the ingester version can't be determined (and a legacy route probe
86
+ * didn't prove a route missing) nothing is reported, so the check has no
87
+ * false positives.
88
+ */
89
+ export function evaluateCompat(input) {
90
+ const manifest = input.manifest ?? COMPAT_MANIFEST;
91
+ const issues = [];
92
+ const seen = new Set();
93
+ for (const id of input.features) {
94
+ if (seen.has(id))
95
+ continue;
96
+ seen.add(id);
97
+ const feature = featureById(id, manifest);
98
+ if (!feature)
99
+ continue;
100
+ const sdkMin = input.sdk ? feature.sdks[input.sdk.name] : undefined;
101
+ if (input.sdk && sdkMin && versionAtLeast(input.sdk.version, sdkMin) === false) {
102
+ const fix = sdkUpgradeHint(input.sdk.name, manifest);
103
+ issues.push({
104
+ feature: id,
105
+ title: feature.title,
106
+ kind: "sdk_too_old",
107
+ component: input.sdk.name,
108
+ required: sdkMin,
109
+ actual: input.sdk.version,
110
+ message: `${feature.title} needs ${input.sdk.name} >= ${sdkMin}; yours is ${input.sdk.version}. ${fix}`,
111
+ fix,
112
+ });
113
+ }
114
+ const ingester = input.ingester;
115
+ if (!ingester)
116
+ continue;
117
+ const fix = ingesterUpgradeHint(manifest);
118
+ const atLeast = versionAtLeast(ingester.version, feature.ingester);
119
+ if (atLeast === false) {
120
+ issues.push({
121
+ feature: id,
122
+ title: feature.title,
123
+ kind: "ingester_too_old",
124
+ component: INGESTER_PACKAGE,
125
+ required: feature.ingester,
126
+ actual: ingester.version,
127
+ message: `${feature.title} needs ingester >= ${feature.ingester}; yours is ${ingester.version}. ${fix}`,
128
+ fix,
129
+ });
130
+ continue;
131
+ }
132
+ if (ingester.version === null &&
133
+ feature.route &&
134
+ ingester.routes?.[feature.route] === false) {
135
+ const actual = "1.4.0 or older (no /v1/compat)";
136
+ issues.push({
137
+ feature: id,
138
+ title: feature.title,
139
+ kind: "ingester_too_old",
140
+ component: INGESTER_PACKAGE,
141
+ required: feature.ingester,
142
+ actual,
143
+ message: `${feature.title} needs ingester >= ${feature.ingester}; yours is ${actual} and has no ${feature.route} route, so this data is dropped. ${fix}`,
144
+ fix,
145
+ });
146
+ continue;
147
+ }
148
+ const schema = ingester.schema;
149
+ if (atLeast && schema && feature.migrations.length) {
150
+ const applied = new Set(schema.applied);
151
+ const missing = feature.migrations.filter((m) => !applied.has(m));
152
+ if (schema.status === "failed" || (schema.status === "ready" && missing.length)) {
153
+ const schemaFix = "Check the ingester's CLICKHOUSE_URL/credentials and its logs, then restart it so the boot migrations run. " +
154
+ `See ${manifest.docs}`;
155
+ const what = missing.length ? missing.join(", ") : feature.migrations.join(", ");
156
+ issues.push({
157
+ feature: id,
158
+ title: feature.title,
159
+ kind: "schema_not_applied",
160
+ component: INGESTER_PACKAGE,
161
+ required: feature.migrations.join(", "),
162
+ actual: schema.status === "failed" ? "migrations failed" : `missing ${what}`,
163
+ message: `${feature.title} needs ClickHouse migration ${what}, which ingester ${ingester.version} has not applied (schema ${schema.status}). ${schemaFix}`,
164
+ fix: schemaFix,
165
+ });
166
+ }
167
+ }
168
+ }
169
+ return issues;
170
+ }
171
+ export function buildCompatReport(input) {
172
+ const manifest = input.manifest ?? COMPAT_MANIFEST;
173
+ const applied = new Set(input.applied);
174
+ const appliedOrdered = input.allMigrations.filter((id) => applied.has(id));
175
+ return {
176
+ ingester: { name: INGESTER_PACKAGE, version: input.version },
177
+ schema: {
178
+ status: input.schemaStatus,
179
+ level: appliedOrdered[appliedOrdered.length - 1] ?? "",
180
+ latest: input.allMigrations[input.allMigrations.length - 1] ?? "",
181
+ applied: appliedOrdered,
182
+ },
183
+ features: manifest.features.map((feature) => {
184
+ const ingesterSupported = versionAtLeast(input.version, feature.ingester) !== false;
185
+ const missingMigrations = feature.migrations.filter((m) => !applied.has(m));
186
+ return {
187
+ ...feature,
188
+ ingesterSupported,
189
+ missingMigrations,
190
+ available: ingesterSupported &&
191
+ (feature.migrations.length === 0 ||
192
+ (input.schemaStatus === "ready" && missingMigrations.length === 0)),
193
+ };
194
+ }),
195
+ manifestVersion: manifest.manifestVersion,
196
+ docs: manifest.docs,
197
+ upgrade: manifest.upgrade,
198
+ };
199
+ }
200
+ /** IngesterCompatInfo from a /v1/compat response body (defensive: it's network input). */
201
+ export function ingesterInfoFromReport(body) {
202
+ if (!body || typeof body !== "object")
203
+ return null;
204
+ const report = body;
205
+ const version = report.ingester?.version;
206
+ if (typeof version !== "string" || !parseVersion(version))
207
+ return null;
208
+ const schema = report.schema;
209
+ const statuses = ["ready", "pending", "failed", "unconfigured"];
210
+ return {
211
+ version,
212
+ ...(schema && statuses.includes(schema.status) && Array.isArray(schema.applied)
213
+ ? {
214
+ schema: {
215
+ status: schema.status,
216
+ applied: schema.applied.filter((id) => typeof id === "string"),
217
+ },
218
+ }
219
+ : {}),
220
+ };
221
+ }
package/dist/config.d.ts CHANGED
@@ -32,10 +32,33 @@ export interface IngesterConfig {
32
32
  rateLimitPerMinute: number;
33
33
  /** Per-key requests per minute for publishable client keys. */
34
34
  clientRateLimitPerMinute: number;
35
+ /** ClickHouse dedupe lookups per tenant per minute for /v1/logs promotion. */
36
+ promotionLookupsPerMinute?: number;
35
37
  /** Retention, overridable per deployment. */
36
38
  occurrenceTtlDays: number;
37
39
  spanTtlDays: number;
38
40
  metricsTtlDays: number;
39
41
  llmCallTtlDays: number;
42
+ /**
43
+ * runtime_logs retention (LOG_TTL_DAYS, default 14). Request summaries are
44
+ * always kept (never sampled), so this is the main volume knob. Applied to
45
+ * fresh tables by the baseline and to existing ones at boot.
46
+ */
47
+ logTtlDays: number;
48
+ /**
49
+ * Extra regex sources scrubbed from stored/forwarded free text and
50
+ * attribute values, and extra regex sources for sensitive attribute
51
+ * keys — on top of the built-in secret/PII patterns (see redact.ts).
52
+ * Env: AUTTER_REDACT_VALUE_PATTERNS / AUTTER_REDACT_KEY_PATTERNS (JSON
53
+ * arrays of strings).
54
+ */
55
+ redactValuePatterns?: string[];
56
+ redactKeyPatterns?: string[];
57
+ /**
58
+ * In-memory budget (MiB, estimated decoded size) for parsed source maps
59
+ * used to symbolicate browser frames at ingest (SOURCE_MAP_CACHE_MB,
60
+ * default 64).
61
+ */
62
+ sourceMapCacheMb?: number;
40
63
  }
41
64
  export declare function loadConfig(): IngesterConfig;
package/dist/config.js CHANGED
@@ -5,6 +5,20 @@ function intEnv(name, fallback) {
5
5
  const value = Number.parseInt(raw, 10);
6
6
  return Number.isFinite(value) && value > 0 ? value : fallback;
7
7
  }
8
+ /** JSON array of regex sources; invalid input fails startup loudly rather
9
+ * than silently running without the operator's extra patterns. */
10
+ function patternListEnv(name) {
11
+ const raw = process.env[name];
12
+ if (!raw)
13
+ return [];
14
+ const parsed = JSON.parse(raw);
15
+ if (!Array.isArray(parsed) || !parsed.every((p) => typeof p === "string")) {
16
+ throw new Error(`${name} must be a JSON array of regex source strings`);
17
+ }
18
+ for (const source of parsed)
19
+ new RegExp(source);
20
+ return parsed;
21
+ }
8
22
  function parseIngestKeys(raw) {
9
23
  if (!raw)
10
24
  return [];
@@ -42,12 +56,17 @@ export function loadConfig() {
42
56
  maxBodyBytes: intEnv("MAX_BODY_BYTES", 1024 * 1024),
43
57
  rateLimitPerMinute: intEnv("RATE_LIMIT_PER_MINUTE", 300),
44
58
  clientRateLimitPerMinute: intEnv("CLIENT_RATE_LIMIT_PER_MINUTE", 120),
59
+ promotionLookupsPerMinute: intEnv("PROMOTION_LOOKUPS_PER_MINUTE", 30),
45
60
  occurrenceTtlDays: intEnv("OCCURRENCE_TTL_DAYS", 14),
46
61
  spanTtlDays: intEnv("SPAN_TTL_DAYS", 7),
47
62
  metricsTtlDays: intEnv("METRICS_TTL_DAYS", 90),
48
63
  // LLM calls keep the metrics horizon, not the span one — cost trends
49
64
  // need months, and per-call volume is small next to HTTP spans.
50
65
  llmCallTtlDays: intEnv("LLM_CALL_TTL_DAYS", 90),
66
+ logTtlDays: intEnv("LOG_TTL_DAYS", 14),
67
+ redactValuePatterns: patternListEnv("AUTTER_REDACT_VALUE_PATTERNS"),
68
+ redactKeyPatterns: patternListEnv("AUTTER_REDACT_KEY_PATTERNS"),
69
+ sourceMapCacheMb: intEnv("SOURCE_MAP_CACHE_MB", 64),
51
70
  };
52
71
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(config.clickhouseDatabase)) {
53
72
  throw new Error(`Invalid CLICKHOUSE_DATABASE name: ${config.clickhouseDatabase}`);
package/dist/context.js CHANGED
@@ -1,24 +1,32 @@
1
+ import { isSensitiveKey, scrubText } from "./redact.js";
2
+ import { BREADCRUMBS_ATTRIBUTE } from "./breadcrumbs.js";
1
3
  /** Bounded, defensive privacy boundary for custom telemetry from any OTLP SDK. */
2
4
  export function sanitizeRuntimeContext(input) {
3
5
  let budget = 512;
4
6
  const seen = new WeakSet();
5
- const scrub = (value) => value
6
- .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted]")
7
- .replace(/\b(?:bearer\s+[A-Za-z0-9._~+\/=~-]{10,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|gh[pousr]_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|autter_(?:rt|pat)_[A-Za-z0-9_-]{10,}|(?:AKIA|ASIA)[A-Z0-9]{16})\b/gi, "[redacted]")
8
- .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, "[redacted]")
9
- .replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi, "$1[redacted]@")
10
- .replace(/(https?:\/\/[^\s?#]+)[?#][^\s]*/gi, "$1");
7
+ // Shared secret/PII patterns (redact.ts), then drop URL query strings
8
+ // and fragments outright — whitelist-style privacy for free text.
9
+ const scrub = (value) => scrubText(value)
10
+ // Query strings: procedural (a `[^\s?#]+[?#]` regex is quadratic on
11
+ // long runs of URLs without a query).
12
+ .replace(/https?:\/\/[^\s]*/gi, (url) => {
13
+ const rest = url.slice(url.indexOf("://") + 3);
14
+ const cut = rest.search(/[?#]/);
15
+ return cut <= 0 ? url : url.slice(0, url.length - rest.length + cut);
16
+ });
11
17
  const visit = (value, key, depth) => {
12
18
  if (--budget < 0 || depth > 6)
13
19
  return "[truncated]";
14
20
  if (/__proto__|constructor|prototype/i.test(key))
15
21
  return undefined;
16
- const usage = /(?:^|\.)(?:input|output|total|prompt|completion)_?tokens$/i.test(key) &&
22
+ const usage =
23
+ // Numeric token COUNTS (incl. the AI rollup's cache_read_tokens) are
24
+ // usage, not secrets.
25
+ /(?:^|\.)(?:input|output|total|prompt|completion|cache_read|cache_creation)_?tokens$/i.test(key) &&
17
26
  typeof value === "number" &&
18
27
  Number.isFinite(value) &&
19
28
  value >= 0;
20
- if (!usage &&
21
- /password|passwd|secret|token|credential|authorization|cookie|email|phone|ssn|card[._-]?number|connection[._-]?string|api[._-]?key|private[._-]?key|request[._-]?body|response[._-]?body|headers|url[._-]?query/i.test(key))
29
+ if (!usage && key && isSensitiveKey(key))
22
30
  return "[redacted]";
23
31
  if (typeof value === "string") {
24
32
  // Serialized custom context crosses the same privacy boundary as nested values.
@@ -33,7 +41,10 @@ export function sanitizeRuntimeContext(input) {
33
41
  const text = /(?:url|path|route|target)$/i.test(key)
34
42
  ? value.split(/[?#]/)[0]
35
43
  : value;
36
- return scrub(text).slice(0, /stack/i.test(key) ? 32000 : 2048);
44
+ // Cut before scrubbing so scrub work is bounded by the stored size;
45
+ // the slack keeps a secret straddling the cut whole for the scrubber.
46
+ const limit = /stack/i.test(key) ? 32000 : 2048;
47
+ return scrub(text.slice(0, limit + 512)).slice(0, limit);
37
48
  }
38
49
  if (typeof value === "number")
39
50
  return Number.isFinite(value) ? value : undefined;
@@ -87,6 +98,8 @@ export function decodeOtlpAttributes(attributes) {
87
98
  .sort((a, b) => Number(/^autter\.(?:operation|event)\./.test(b.key ?? "")) -
88
99
  Number(/^autter\.(?:operation|event)\./.test(a.key ?? "")))
89
100
  .slice(0, 128)
90
- .filter((a) => a.key && !/^(?:__proto__|constructor|prototype)$/.test(a.key))
101
+ .filter(
102
+ // Breadcrumbs are their own validated field (breadcrumbs.ts), never a plain attribute.
103
+ (a) => a.key && a.key !== BREADCRUMBS_ATTRIBUTE && !/^(?:__proto__|constructor|prototype)$/.test(a.key))
91
104
  .map((a) => [a.key, decode(a.value)])));
92
105
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Declared error metadata on the wire (span/event attributes, log
3
+ * attributes, browser context keys):
4
+ *
5
+ * autter.error.code string matching CODE_PATTERN (≤80) — grouping key
6
+ * autter.error.why declared cause (≤1000 chars)
7
+ * autter.error.fix declared remedy (≤1000 chars)
8
+ * autter.error.link docs link (http(s) only, ≤500 chars)
9
+ * autter.error.expected boolean — expected business failure
10
+ * autter.request.id request id joining the error to its summary
11
+ *
12
+ * Every field is optional and validated independently: a bad value is
13
+ * dropped on its own and never rejects the signal. Values are lifted onto
14
+ * dedicated occurrence fields (and columns) instead of living only in the
15
+ * attributes JSON, because the generic context sanitiser rewrites URLs
16
+ * (query AND fragment stripped) — the declared link keeps its #anchor here.
17
+ */
18
+ export declare const REQUEST_ID_PATTERN: RegExp;
19
+ export interface DeclaredErrorFields {
20
+ errorCode?: string;
21
+ why?: string;
22
+ fix?: string;
23
+ link?: string;
24
+ expected?: boolean;
25
+ requestId?: string;
26
+ }
27
+ type AttributeSource = Map<string, string> | Record<string, unknown> | null | undefined;
28
+ /**
29
+ * http(s) only; credentials and query string removed (a docs link never
30
+ * needs them), fragment kept. Over-long links are dropped rather than
31
+ * truncated — a cut URL is a broken link.
32
+ */
33
+ export declare function declaredLink(value: unknown): string | undefined;
34
+ export declare function validRequestId(value: unknown): string | undefined;
35
+ /**
36
+ * Lift the declared fields from one or more attribute sources; the first
37
+ * source carrying a key wins (e.g. exception event before its span).
38
+ * Returns only the fields that are present and valid.
39
+ */
40
+ export declare function liftErrorFields(...sources: AttributeSource[]): DeclaredErrorFields;
41
+ export {};