@autter/otlp-ingester 1.6.0 → 1.7.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/README.md CHANGED
@@ -31,6 +31,16 @@ works with its default exporter settings.
31
31
  | --- | --- | --- | --- |
32
32
  | `server` (default) | `autter_rt_…` (secret) | all endpoints | 300 req/min |
33
33
  | `client` | `autter_rtc_…` (publishable, safe in frontend bundles) | `/v1/browser` only | origin allow-list, 120 req/min |
34
+ | `upload` (1.7.0+) | `autter_rtu_…` (secret, for CI) | `/v1/sourcemaps` and `/v1/sourcemaps/check` only | 300 req/min |
35
+
36
+ An upload key gets `403 {"error":"upload_key_not_allowed","detail":"autter_rtu_ keys can only upload source maps …"}`
37
+ on every other route, so a CI secret that leaks can upload source maps for
38
+ one repository and nothing else. A static entry without `scope` is an upload
39
+ key when its key starts with `autter_rtu_`, else a server key; an entry (or a
40
+ validator answer) with any other `scope` is ignored rather than treated as a
41
+ server key. Ingesters before 1.7.0 treat every scope but `client` as
42
+ `server`, so a validator must not answer `"scope": "upload"` to them: from
43
+ 1.7.0 every validator request carries `x-autter-ingester-version`.
34
44
 
35
45
  `/v1/browser` answers CORS preflights permissively; real enforcement (key +
36
46
  origin allow-list) happens on the POST. Cross-origin browsers send
@@ -41,12 +51,14 @@ route parses as JSON.
41
51
  AUTTER_INGEST_KEYS='[
42
52
  {"key":"autter_rt_…","orgId":"org1","repositoryId":"repo1"},
43
53
  {"key":"autter_rtc_…","orgId":"org1","repositoryId":"repo1",
44
- "scope":"client","allowedOrigins":["https://app.example.com"]}
54
+ "scope":"client","allowedOrigins":["https://app.example.com"]},
55
+ {"key":"autter_rtu_…","orgId":"org1","repositoryId":"repo1","scope":"upload"}
45
56
  ]'
46
57
  ```
47
58
 
48
59
  The validator webhook may return the same extra fields:
49
- `{ orgId, repositoryId, scope?, allowedOrigins? }`.
60
+ `{ orgId, repositoryId, scope?, allowedOrigins? }`, with `scope` one of
61
+ `server`, `client`, `upload`.
50
62
 
51
63
  ### Version compatibility
52
64
 
@@ -75,7 +87,7 @@ the Autter dashboard reads both. See [docs/COMPATIBILITY.md](../../docs/COMPATIB
75
87
  | `SINK_MAX_ATTEMPTS` | `12` | Delivery attempts per batch (1–60 s backoff) |
76
88
  | `SINK_MAX_BUFFERED_BATCHES` | `1000` | Retry buffer cap; oldest drops are logged |
77
89
  | `SINK_MAX_BUFFERED_MB` | `64` | Sink retry buffer cap (memory) |
78
- | `SOURCE_MAP_CACHE_MB` | `64` | Memory budget (estimated decoded size) for parsed source maps used to symbolicate browser frames at ingest; least recently used maps are evicted |
90
+ | `SOURCE_MAP_CACHE_MB` | `64` | Memory budget (decoded size, function scope tables included) for parsed source maps used to symbolicate browser frames at ingest; least recently used maps are evicted, and a map that needs more than half of it is used without being kept |
79
91
  | `MAX_BODY_BYTES` | `1048576` | Request body cap |
80
92
  | `RATE_LIMIT_PER_MINUTE` | `300` | Per-key fixed window (server keys) |
81
93
  | `CLIENT_RATE_LIMIT_PER_MINUTE` | `120` | Per-key fixed window (client keys) |
package/dist/auth.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Request } from "express";
2
- import type { IngesterConfig } from "./config.js";
2
+ import { type IngesterConfig } from "./config.js";
3
3
  import type { IngestContext } from "./types.js";
4
4
  export declare class KeyResolver {
5
5
  private readonly config;
package/dist/auth.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { INGESTER_VERSION_HEADER } from "./compat.js";
2
+ import { defaultScopeFor, isIngestKeyScope } from "./config.js";
3
+ import { ingesterVersion } from "./sdk-versions.js";
1
4
  const VALIDATOR_CACHE_TTL_MS = 60_000;
2
5
  export class KeyResolver {
3
6
  config;
@@ -6,10 +9,13 @@ export class KeyResolver {
6
9
  constructor(config) {
7
10
  this.config = config;
8
11
  for (const entry of config.ingestKeys) {
12
+ const scope = entry.scope ?? defaultScopeFor(entry.key);
13
+ if (!isIngestKeyScope(scope))
14
+ continue;
9
15
  this.staticKeys.set(entry.key, {
10
16
  orgId: entry.orgId,
11
17
  repositoryId: entry.repositoryId,
12
- scope: entry.scope ?? "server",
18
+ scope,
13
19
  allowedOrigins: entry.allowedOrigins ?? [],
14
20
  });
15
21
  }
@@ -44,6 +50,11 @@ export class KeyResolver {
44
50
  method: "POST",
45
51
  headers: {
46
52
  "content-type": "application/json",
53
+ // Lets the validator tell which scopes this ingester enforces:
54
+ // before 1.7.0 any scope but "client" meant a server key, so an
55
+ // upload key must not be answered as "upload" to an ingester
56
+ // that doesn't send this header (or sends an older version).
57
+ [INGESTER_VERSION_HEADER]: ingesterVersion(),
47
58
  ...(this.config.keyValidatorToken
48
59
  ? { authorization: `Bearer ${this.config.keyValidatorToken}` }
49
60
  : {}),
@@ -53,11 +64,12 @@ export class KeyResolver {
53
64
  });
54
65
  if (response.ok) {
55
66
  const body = (await response.json());
56
- if (body.orgId && body.repositoryId) {
67
+ const scope = body.scope === undefined || body.scope === null ? defaultScopeFor(key) : body.scope;
68
+ if (body.orgId && body.repositoryId && isIngestKeyScope(scope)) {
57
69
  ctx = {
58
70
  orgId: body.orgId,
59
71
  repositoryId: body.repositoryId,
60
- scope: body.scope ?? "server",
72
+ scope,
61
73
  allowedOrigins: body.allowedOrigins ?? [],
62
74
  };
63
75
  }
@@ -19,6 +19,9 @@ const INSERT_SETTINGS = {
19
19
  async_insert: 1,
20
20
  wait_for_async_insert: 1,
21
21
  };
22
+ /** Source maps above this go in with a plain insert (below ClickHouse's
23
+ * default async_insert_max_data_size of 10 MiB). */
24
+ const ASYNC_INSERT_MAX_MAP_CHARS = 8 * 1024 * 1024;
22
25
  /** "YYYY-MM-DD hh:mm:ss.sss" (UTC) — the DateTime64 query-parameter form. */
23
26
  function chDateTime(date) {
24
27
  return date.toISOString().replace("T", " ").replace("Z", "");
@@ -474,7 +477,15 @@ export class ClickHouseStore {
474
477
  await this.ensureSchema();
475
478
  await this.getClient().insert({
476
479
  table: this.table("runtime_source_maps"), format: "JSONEachRow",
477
- clickhouse_settings: INSERT_SETTINGS,
480
+ // One row of up to 32 MiB. Parallel parsing cuts input into
481
+ // 10 MiB chunks, refuses single JSON rows far past that, and gains
482
+ // nothing on one row. A map past the async-insert buffer (10 MiB
483
+ // by default) is inserted directly rather than buffered.
484
+ clickhouse_settings: {
485
+ ...INSERT_SETTINGS,
486
+ input_format_parallel_parsing: 0,
487
+ ...(sourceMap.map.length > ASYNC_INSERT_MAX_MAP_CHARS ? { async_insert: 0 } : {}),
488
+ },
478
489
  values: [{ org_id: ctx.orgId, repository_id: ctx.repositoryId, release: sourceMap.release,
479
490
  filename: sourceMap.filename, map: sourceMap.map, debug_id: sourceMap.debugId,
480
491
  content_hash: sourceMap.contentHash }],
@@ -118,6 +118,22 @@
118
118
  "ingester": "1.6.0",
119
119
  "migrations": [],
120
120
  "sdks": { "@autter/runtime-browser": "1.6.0", "@autter/runtime-next": "1.6.0", "@autter/runtime-node": "1.6.0" }
121
+ },
122
+ {
123
+ "id": "sourcemap_upload_keys",
124
+ "title": "Upload-only keys for source maps (autter_rtu_)",
125
+ "ingester": "1.7.0",
126
+ "migrations": [],
127
+ "route": "/v1/sourcemaps",
128
+ "sdks": {}
129
+ },
130
+ {
131
+ "id": "source_map_verify",
132
+ "title": "Source map check by debug id (autter-runtime sourcemaps verify)",
133
+ "ingester": "1.7.0",
134
+ "migrations": ["0017-source-maps-debug-id"],
135
+ "route": "/v1/sourcemaps/check",
136
+ "sdks": {}
121
137
  }
122
138
  ]
123
139
  }
package/dist/config.d.ts CHANGED
@@ -1,9 +1,26 @@
1
+ import type { IngestKeyScope } from "./types.js";
2
+ export declare const INGEST_KEY_SCOPES: readonly IngestKeyScope[];
3
+ /** Prefix of upload-only keys (`autter_rtu_…`): source map uploads from CI. */
4
+ export declare const UPLOAD_KEY_PREFIX = "autter_rtu_";
5
+ export declare function isIngestKeyScope(value: unknown): value is IngestKeyScope;
6
+ /**
7
+ * The scope of a key whose static entry or validator answer names none:
8
+ * "upload" for an `autter_rtu_…` key (its prefix promises no more than
9
+ * that), else "server" as before 1.7.0.
10
+ */
11
+ export declare function defaultScopeFor(key: string): IngestKeyScope;
1
12
  export interface StaticIngestKey {
2
13
  key: string;
3
14
  orgId: string;
4
15
  repositoryId: string;
5
- /** "server" (default) = secret backend key; "client" = publishable browser key. */
6
- scope?: "client" | "server";
16
+ /**
17
+ * "server" = secret backend key, every route; "client" = publishable
18
+ * browser key, `/v1/browser` only; "upload" = CI key, `/v1/sourcemaps`
19
+ * and `/v1/sourcemaps/check` only. Default: "upload" for an
20
+ * `autter_rtu_…` key, else "server". An entry with any other value is
21
+ * ignored (never treated as a server key).
22
+ */
23
+ scope?: IngestKeyScope;
7
24
  /** client keys: exact origins allowed to send (e.g. https://app.example.com). */
8
25
  allowedOrigins?: string[];
9
26
  }
@@ -55,10 +72,14 @@ export interface IngesterConfig {
55
72
  redactValuePatterns?: string[];
56
73
  redactKeyPatterns?: string[];
57
74
  /**
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).
75
+ * In-memory budget (MiB, decoded size) for parsed source maps used to
76
+ * symbolicate browser frames at ingest (SOURCE_MAP_CACHE_MB, default 64).
77
+ * A map that needs more than half of it is used without being kept.
61
78
  */
62
79
  sourceMapCacheMb?: number;
63
80
  }
81
+ /** Parse AUTTER_INGEST_KEYS. Entries without a string key, orgId and
82
+ * repositoryId, or with an unknown scope, are dropped (a typo in `scope`
83
+ * must not turn a restricted key into a server key). */
84
+ export declare function parseIngestKeys(raw: string | undefined): StaticIngestKey[];
64
85
  export declare function loadConfig(): IngesterConfig;
package/dist/config.js CHANGED
@@ -1,3 +1,17 @@
1
+ export const INGEST_KEY_SCOPES = ["server", "client", "upload"];
2
+ /** Prefix of upload-only keys (`autter_rtu_…`): source map uploads from CI. */
3
+ export const UPLOAD_KEY_PREFIX = "autter_rtu_";
4
+ export function isIngestKeyScope(value) {
5
+ return typeof value === "string" && INGEST_KEY_SCOPES.includes(value);
6
+ }
7
+ /**
8
+ * The scope of a key whose static entry or validator answer names none:
9
+ * "upload" for an `autter_rtu_…` key (its prefix promises no more than
10
+ * that), else "server" as before 1.7.0.
11
+ */
12
+ export function defaultScopeFor(key) {
13
+ return key.startsWith(UPLOAD_KEY_PREFIX) ? "upload" : "server";
14
+ }
1
15
  function intEnv(name, fallback) {
2
16
  const raw = process.env[name];
3
17
  if (!raw)
@@ -19,22 +33,33 @@ function patternListEnv(name) {
19
33
  new RegExp(source);
20
34
  return parsed;
21
35
  }
22
- function parseIngestKeys(raw) {
36
+ /** Parse AUTTER_INGEST_KEYS. Entries without a string key, orgId and
37
+ * repositoryId, or with an unknown scope, are dropped (a typo in `scope`
38
+ * must not turn a restricted key into a server key). */
39
+ export function parseIngestKeys(raw) {
23
40
  if (!raw)
24
41
  return [];
42
+ let parsed;
25
43
  try {
26
- const parsed = JSON.parse(raw);
27
- if (!Array.isArray(parsed))
28
- return [];
29
- return parsed.filter((entry) => entry &&
30
- typeof entry.key === "string" &&
31
- typeof entry.orgId === "string" &&
32
- typeof entry.repositoryId === "string");
44
+ parsed = JSON.parse(raw);
33
45
  }
34
46
  catch {
35
47
  console.error("AUTTER_INGEST_KEYS is not valid JSON — ignoring");
36
48
  return [];
37
49
  }
50
+ if (!Array.isArray(parsed))
51
+ return [];
52
+ const keys = [];
53
+ parsed.forEach((entry, index) => {
54
+ if (!entry || typeof entry.key !== "string" || typeof entry.orgId !== "string" || typeof entry.repositoryId !== "string")
55
+ return;
56
+ if (entry.scope !== undefined && !isIngestKeyScope(entry.scope)) {
57
+ console.error(`AUTTER_INGEST_KEYS[${index}] has scope ${JSON.stringify(entry.scope)}; expected one of ${INGEST_KEY_SCOPES.join(", ")} — ignoring this key`);
58
+ return;
59
+ }
60
+ keys.push({ ...entry, scope: entry.scope ?? defaultScopeFor(entry.key) });
61
+ });
62
+ return keys;
38
63
  }
39
64
  export function loadConfig() {
40
65
  const config = {
@@ -52,65 +52,65 @@ export declare const browserPayloadSchema: z.ZodObject<{
52
52
  */
53
53
  traceId: z.ZodCatch<z.ZodOptional<z.ZodString>>;
54
54
  }, "strip", z.ZodTypeAny, {
55
+ type: "request_failure" | "timing" | "outcome" | "csp_violation" | "message" | "exception" | "unhandled_rejection" | "session_start" | "track_event";
55
56
  message: string;
56
- type: "message" | "exception" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
57
57
  timestamp: string;
58
+ breadcrumbs?: unknown[] | undefined;
59
+ route?: string | undefined;
58
60
  severity?: "info" | "warning" | "error" | "fatal" | undefined;
59
61
  errorType?: string | undefined;
60
62
  stack?: string | undefined;
61
- route?: string | undefined;
62
63
  traceId?: string | undefined;
63
- breadcrumbs?: unknown[] | undefined;
64
64
  debugIds?: Record<string, unknown> | undefined;
65
65
  durationMs?: number | undefined;
66
66
  filename?: string | undefined;
67
- id?: string | undefined;
68
- name?: string | undefined;
69
- line?: number | undefined;
70
67
  column?: number | undefined;
68
+ line?: number | undefined;
69
+ name?: string | undefined;
70
+ id?: string | undefined;
71
71
  context?: Record<string, unknown> | undefined;
72
72
  fingerprint?: string | string[] | undefined;
73
73
  }, {
74
- type: "message" | "exception" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
74
+ type: "request_failure" | "timing" | "outcome" | "csp_violation" | "message" | "exception" | "unhandled_rejection" | "session_start" | "track_event";
75
75
  timestamp: string;
76
+ breadcrumbs?: unknown;
77
+ route?: string | undefined;
76
78
  message?: string | undefined;
77
79
  severity?: "info" | "warning" | "error" | "fatal" | undefined;
78
80
  errorType?: string | undefined;
79
81
  stack?: string | undefined;
80
- route?: string | undefined;
81
82
  traceId?: unknown;
82
- breadcrumbs?: unknown;
83
83
  debugIds?: unknown;
84
84
  durationMs?: number | undefined;
85
85
  filename?: string | undefined;
86
- id?: string | undefined;
87
- name?: string | undefined;
88
- line?: number | undefined;
89
86
  column?: number | undefined;
87
+ line?: number | undefined;
88
+ name?: string | undefined;
89
+ id?: string | undefined;
90
90
  context?: Record<string, unknown> | undefined;
91
91
  fingerprint?: unknown;
92
92
  }>, "many">;
93
93
  }, "strip", z.ZodTypeAny, {
94
+ version: 1;
94
95
  service: string;
95
96
  environment: string;
96
- version: 1;
97
97
  events: {
98
+ type: "request_failure" | "timing" | "outcome" | "csp_violation" | "message" | "exception" | "unhandled_rejection" | "session_start" | "track_event";
98
99
  message: string;
99
- type: "message" | "exception" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
100
100
  timestamp: string;
101
+ breadcrumbs?: unknown[] | undefined;
102
+ route?: string | undefined;
101
103
  severity?: "info" | "warning" | "error" | "fatal" | undefined;
102
104
  errorType?: string | undefined;
103
105
  stack?: string | undefined;
104
- route?: string | undefined;
105
106
  traceId?: string | undefined;
106
- breadcrumbs?: unknown[] | undefined;
107
107
  debugIds?: Record<string, unknown> | undefined;
108
108
  durationMs?: number | undefined;
109
109
  filename?: string | undefined;
110
- id?: string | undefined;
111
- name?: string | undefined;
112
- line?: number | undefined;
113
110
  column?: number | undefined;
111
+ line?: number | undefined;
112
+ name?: string | undefined;
113
+ id?: string | undefined;
114
114
  context?: Record<string, unknown> | undefined;
115
115
  fingerprint?: string | string[] | undefined;
116
116
  }[];
@@ -118,26 +118,26 @@ export declare const browserPayloadSchema: z.ZodObject<{
118
118
  sessionId?: string | undefined;
119
119
  sdk?: string | undefined;
120
120
  }, {
121
+ version: 1;
121
122
  service: string;
122
123
  environment: string;
123
- version: 1;
124
124
  events: {
125
- type: "message" | "exception" | "request_failure" | "timing" | "outcome" | "csp_violation" | "unhandled_rejection" | "session_start" | "track_event";
125
+ type: "request_failure" | "timing" | "outcome" | "csp_violation" | "message" | "exception" | "unhandled_rejection" | "session_start" | "track_event";
126
126
  timestamp: string;
127
+ breadcrumbs?: unknown;
128
+ route?: string | undefined;
127
129
  message?: string | undefined;
128
130
  severity?: "info" | "warning" | "error" | "fatal" | undefined;
129
131
  errorType?: string | undefined;
130
132
  stack?: string | undefined;
131
- route?: string | undefined;
132
133
  traceId?: unknown;
133
- breadcrumbs?: unknown;
134
134
  debugIds?: unknown;
135
135
  durationMs?: number | undefined;
136
136
  filename?: string | undefined;
137
- id?: string | undefined;
138
- name?: string | undefined;
139
- line?: number | undefined;
140
137
  column?: number | undefined;
138
+ line?: number | undefined;
139
+ name?: string | undefined;
140
+ id?: string | undefined;
141
141
  context?: Record<string, unknown> | undefined;
142
142
  fingerprint?: unknown;
143
143
  }[];
@@ -78,7 +78,13 @@ export interface OtlpMetricsRequest {
78
78
  }>;
79
79
  }
80
80
  export interface NormalizedTraces {
81
+ /** One per thrown error: duplicate recordings across a trace's spans are
82
+ * merged and redundant status-only occurrences dropped (trace-dedupe.ts). */
81
83
  occurrences: RuntimeOccurrenceInput[];
84
+ /** Each occurrence's position among every signal the batch recorded,
85
+ * before deduplication: the batch index its deterministic id is derived
86
+ * from, so a merge or a drop never shifts another occurrence's id. */
87
+ occurrenceBatchIndexes: number[];
82
88
  spans: RuntimeSpanRow[];
83
89
  metricPoints: RuntimeMetricPoint[];
84
90
  llmCalls: RuntimeLlmCall[];