@downtrace/protocol 0.4.0 → 0.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ The ingestion contract between Downtrace agents and the Downtrace cloud, as a JS
4
4
 
5
5
  - `schema/v0/aggregates.schema.json` — the contract. Everything else derives from it.
6
6
  - `schema/v0/fixtures/{valid,invalid}/` — examples every implementation must accept and reject.
7
- - Exports: `PROTOCOL_VERSION`, `AGGREGATES_PATH`, `AGGREGATES_SCHEMA_V0`, `LATENCY_BOUNDARIES_V0`, `LATENCY_BUCKETS_V0`, `latencyBucket()`, `CALLS_PER_REQUEST_BOUNDARIES_V0`, `CALLS_PER_REQUEST_BUCKETS_V0`, `callsPerRequestBucket()`, and the types `AggregatesBatch`, `Interval`, `Endpoint`, `LatencyHistogram`, `Dependency`, `PostgresStats`, …
7
+ - Exports: `PROTOCOL_VERSION`, `ACCEPTED_PROTOCOL_VERSIONS_V0`, `AGGREGATES_PATH`, `AGGREGATES_SCHEMA_V0`, `LATENCY_BOUNDARIES_V0`, `LATENCY_BUCKETS_V0`, `latencyBucket()`, `CALLS_PER_REQUEST_BOUNDARIES_V0`, `CALLS_PER_REQUEST_BUCKETS_V0`, `callsPerRequestBucket()`, and the types `AggregatesBatch`, `Interval`, `Endpoint`, `LatencyHistogram`, `Dependency`, `PostgresStats`, …
8
8
 
9
9
  ## v0 in one sentence
10
10
 
@@ -16,7 +16,11 @@ Since **0.2.0** an endpoint may also carry `postgres`: how many queries each req
16
16
 
17
17
  ## How the protocol changes
18
18
 
19
- Fields are only ever **added**, and always optional. Each addition bumps the minor version, and the batch's `protocol` field is an enum of every published minor, so a cloud that speaks 0.2 still accepts an agent that speaks 0.1. Removing or renaming a field, or moving a histogram's bucket bounds, would be a major version on a new path. Bucket bounds are declared in the schema itself (`x-latency-boundaries-ms`, `x-queries-per-request-boundaries`), so they are part of the generated contract rather than a constant someone has to keep in sync.
19
+ **The package version is the protocol version.** `@downtrace/protocol@0.5.0` speaks protocol `0.5.0`, and CI refuses to publish a release where the two disagree, so you can read the contract off the version in your lockfile. A release that changes the package without touching the contract moves the patch segment only.
20
+
21
+ Since **0.6.0** a batch may also carry `profile`: what each route normally does, on its own cadence rather than with every interval. Each operation is a `hash` — the identity, which is what groups them — plus an optional normalised `text`, so a sender who would rather not ship the text of their queries keeps the whole analysis and loses only the label. Queries and error signatures share one shape, so the next kind needs no new field.
22
+
23
+ Fields are only ever **added**, and always optional. Each addition bumps the minor version, and the batch's `protocol` field is an enum of every published minor, so a cloud that speaks 0.2 still accepts an agent that speaks 0.1. Removing or renaming a field, or moving a histogram's bucket bounds, would be a major version on a new path. Bucket bounds are declared in the schema itself (`x-latency-boundaries-ms`, `x-calls-per-request-boundaries`), and so are the versions themselves and the ingestion path (`x-ingest-path`), so they are part of the generated contract rather than a constant someone has to keep in sync.
20
24
 
21
25
  ## Source
22
26
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ //#region src/generated/paths.d.ts
2
+ /** Path, relative to the ingest URL, that receives AggregatesBatch payloads. */
3
+ declare const AGGREGATES_PATH = "/v0/aggregates";
4
+ //#endregion
1
5
  //#region src/generated/aggregates.d.ts
2
6
  /**
3
7
  * A batch of per-interval, per-route aggregates sent by an agent to the cloud (POST /v0/aggregates).
@@ -6,7 +10,7 @@ interface AggregatesBatch {
6
10
  /**
7
11
  * Protocol version this batch conforms to. Every published minor of v0 stays acceptable.
8
12
  */
9
- protocol: "0.1.0" | "0.2.0" | "0.3.0" | "0.4.0" | "0.5.0";
13
+ protocol: "0.1.0" | "0.2.0" | "0.3.0" | "0.4.0" | "0.5.0" | "0.6.0";
10
14
  agent: AgentInfo;
11
15
  instance: InstanceInfo;
12
16
  deploy: DeployInfo;
@@ -15,6 +19,7 @@ interface AggregatesBatch {
15
19
  * @maxItems 6
16
20
  */
17
21
  intervals: [Interval] | [Interval, Interval] | [Interval, Interval, Interval] | [Interval, Interval, Interval, Interval] | [Interval, Interval, Interval, Interval, Interval] | [Interval, Interval, Interval, Interval, Interval, Interval];
22
+ profile?: Profile;
18
23
  }
19
24
  interface AgentInfo {
20
25
  name: string;
@@ -204,6 +209,64 @@ interface RuntimeHealth {
204
209
  */
205
210
  inFlightMax: number;
206
211
  }
212
+ /**
213
+ * What each route normally does, sent once a minute rather than with every interval: a profile changes when the code changes, and at the aggregates' cadence it would not fit in the project's row budget (ADR 0017).
214
+ */
215
+ interface Profile {
216
+ /**
217
+ * Profile window start, Unix epoch milliseconds.
218
+ */
219
+ start: number;
220
+ durationMs: number;
221
+ /**
222
+ * @maxItems 4096
223
+ */
224
+ endpoints: ProfileEndpoint[];
225
+ }
226
+ interface ProfileEndpoint {
227
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "OTHER";
228
+ /**
229
+ * Normalised route template, e.g. /products/:id, or (other).
230
+ */
231
+ route: string;
232
+ /**
233
+ * @maxItems 64
234
+ */
235
+ operations: Operation[];
236
+ }
237
+ /**
238
+ * One thing this route does, identified by a hash and labelled by an optional normalised text. The hash is the identity, so suppressing the text keeps the analysis whole (ADR 0017, invariant 5).
239
+ */
240
+ interface Operation {
241
+ /**
242
+ * What kind of operation this is.
243
+ */
244
+ kind: "query" | "error";
245
+ /**
246
+ * Stable identity of the operation, or `(other)` for the bucket that holds the rest.
247
+ */
248
+ hash: string;
249
+ /**
250
+ * Normalised text, e.g. `SELECT id FROM products WHERE id = ?`. Optional: a user who would rather not send it keeps the whole analysis, only without the label.
251
+ */
252
+ text?: string;
253
+ /**
254
+ * Executions across every request on this route.
255
+ */
256
+ count: number;
257
+ /**
258
+ * Sum of their durations, in milliseconds.
259
+ */
260
+ totalMs: number;
261
+ /**
262
+ * Executions that failed.
263
+ */
264
+ errors?: number;
265
+ /**
266
+ * Only on the `(other)` bucket: how many distinct operations it merges, so a cap does not become a lie by omission.
267
+ */
268
+ distinct?: number;
269
+ }
207
270
  //#endregion
208
271
  //#region src/generated/boundaries.d.ts
209
272
  /** Upper bounds (ms) of the fixed latency buckets; the last bucket is open-ended. */
@@ -215,20 +278,20 @@ declare const CALLS_PER_REQUEST_BOUNDARIES_V0: readonly number[];
215
278
  /** Number of calls-per-request buckets: boundaries + 1 open-ended bucket. */
216
279
  declare const CALLS_PER_REQUEST_BUCKETS_V0 = 8;
217
280
  //#endregion
281
+ //#region src/generated/versions.d.ts
282
+ /** Every published minor of v0, oldest first. The cloud never stops accepting one it published (ADR 0008). */
283
+ declare const ACCEPTED_PROTOCOL_VERSIONS_V0: readonly ["0.1.0", "0.2.0", "0.3.0", "0.4.0", "0.5.0", "0.6.0"];
284
+ /** The newest published minor: what an up-to-date agent stamps on every batch. */
285
+ declare const PROTOCOL_VERSION = "0.6.0";
286
+ //#endregion
218
287
  //#region src/index.d.ts
219
- /**
220
- * Version of the ingestion protocol this package speaks. Every published minor of v0 stays acceptable to the
221
- * cloud, so an agent on an older minor keeps working: fields are only ever added, and always optional (ADR 0008).
222
- */
223
- declare const PROTOCOL_VERSION = "0.5.0";
224
- /** Path, relative to the ingest URL, that receives AggregatesBatch payloads. */
225
- declare const AGGREGATES_PATH = "/v0/aggregates";
226
288
  /** The JSON Schema (draft 2020-12) for AggregatesBatch, for validators on either side. */
227
289
  declare const AGGREGATES_SCHEMA_V0: {
228
290
  $schema: string;
229
291
  $id: string;
230
292
  title: string;
231
293
  description: string;
294
+ "x-ingest-path": string;
232
295
  type: string;
233
296
  additionalProperties: boolean;
234
297
  required: string[];
@@ -255,6 +318,9 @@ declare const AGGREGATES_SCHEMA_V0: {
255
318
  $ref: string;
256
319
  };
257
320
  };
321
+ profile: {
322
+ $ref: string;
323
+ };
258
324
  };
259
325
  $defs: {
260
326
  AgentInfo: {
@@ -588,6 +654,96 @@ declare const AGGREGATES_SCHEMA_V0: {
588
654
  };
589
655
  };
590
656
  };
657
+ Profile: {
658
+ type: string;
659
+ additionalProperties: boolean;
660
+ description: string;
661
+ required: string[];
662
+ properties: {
663
+ start: {
664
+ type: string;
665
+ minimum: number;
666
+ description: string;
667
+ };
668
+ durationMs: {
669
+ type: string;
670
+ minimum: number;
671
+ };
672
+ endpoints: {
673
+ type: string;
674
+ maxItems: number;
675
+ items: {
676
+ $ref: string;
677
+ };
678
+ };
679
+ };
680
+ };
681
+ ProfileEndpoint: {
682
+ type: string;
683
+ additionalProperties: boolean;
684
+ required: string[];
685
+ properties: {
686
+ method: {
687
+ enum: string[];
688
+ };
689
+ route: {
690
+ type: string;
691
+ minLength: number;
692
+ maxLength: number;
693
+ description: string;
694
+ };
695
+ operations: {
696
+ type: string;
697
+ maxItems: number;
698
+ items: {
699
+ $ref: string;
700
+ };
701
+ };
702
+ };
703
+ };
704
+ Operation: {
705
+ type: string;
706
+ additionalProperties: boolean;
707
+ description: string;
708
+ required: string[];
709
+ properties: {
710
+ kind: {
711
+ enum: string[];
712
+ description: string;
713
+ };
714
+ hash: {
715
+ type: string;
716
+ minLength: number;
717
+ maxLength: number;
718
+ description: string;
719
+ };
720
+ text: {
721
+ type: string;
722
+ maxLength: number;
723
+ description: string;
724
+ };
725
+ count: {
726
+ type: string;
727
+ minimum: number;
728
+ description: string;
729
+ };
730
+ totalMs: {
731
+ type: string;
732
+ minimum: number;
733
+ description: string;
734
+ };
735
+ errors: {
736
+ type: string;
737
+ minimum: number;
738
+ description: string;
739
+ };
740
+ distinct: {
741
+ type: string;
742
+ minimum: number;
743
+ description: string;
744
+ };
745
+ };
746
+ };
591
747
  };
592
748
  };
593
749
  /** Index of the bucket a latency (ms) falls into: first boundary >= latency, else the open-ended last bucket. */
@@ -595,4 +751,4 @@ declare function latencyBucket(ms: number): number;
595
751
  /** Index of the bucket a call count falls into: first boundary >= count, else the open-ended last bucket. */
596
752
  declare function callsPerRequestBucket(calls: number): number;
597
753
  //#endregion
598
- export { AGGREGATES_PATH, AGGREGATES_SCHEMA_V0, type AgentInfo, type AggregatesBatch, CALLS_PER_REQUEST_BOUNDARIES_V0, CALLS_PER_REQUEST_BUCKETS_V0, type Dependency, type DeployInfo, type Endpoint, type InstanceInfo, type Interval, LATENCY_BOUNDARIES_V0, LATENCY_BUCKETS_V0, type LatencyHistogram, PROTOCOL_VERSION, type PostgresStats, type RuntimeHealth, type StatusClasses, callsPerRequestBucket, latencyBucket };
754
+ export { ACCEPTED_PROTOCOL_VERSIONS_V0, AGGREGATES_PATH, AGGREGATES_SCHEMA_V0, type AgentInfo, type AggregatesBatch, CALLS_PER_REQUEST_BOUNDARIES_V0, CALLS_PER_REQUEST_BUCKETS_V0, type Dependency, type DeployInfo, type Endpoint, type InstanceInfo, type Interval, LATENCY_BOUNDARIES_V0, LATENCY_BUCKETS_V0, type LatencyHistogram, PROTOCOL_VERSION, type PostgresStats, type RuntimeHealth, type StatusClasses, callsPerRequestBucket, latencyBucket };
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ var aggregates_schema_default = {
4
4
  $id: "https://downtrace.io/schema/v0/aggregates.schema.json",
5
5
  title: "AggregatesBatch",
6
6
  description: "A batch of per-interval, per-route aggregates sent by an agent to the cloud (POST /v0/aggregates).",
7
+ "x-ingest-path": "/v0/aggregates",
7
8
  type: "object",
8
9
  additionalProperties: false,
9
10
  required: [
@@ -21,7 +22,8 @@ var aggregates_schema_default = {
21
22
  "0.2.0",
22
23
  "0.3.0",
23
24
  "0.4.0",
24
- "0.5.0"
25
+ "0.5.0",
26
+ "0.6.0"
25
27
  ],
26
28
  "description": "Protocol version this batch conforms to. Every published minor of v0 stays acceptable."
27
29
  },
@@ -33,7 +35,8 @@ var aggregates_schema_default = {
33
35
  "minItems": 1,
34
36
  "maxItems": 6,
35
37
  "items": { "$ref": "#/$defs/Interval" }
36
- }
38
+ },
39
+ "profile": { "$ref": "#/$defs/Profile" }
37
40
  },
38
41
  $defs: {
39
42
  "AgentInfo": {
@@ -458,6 +461,112 @@ var aggregates_schema_default = {
458
461
  "description": "Time requests spent waiting to be able to talk to this dependency, on top of the time spent talking to it: a saturated connection pool shows up here and nowhere else. Optional; agents that cannot see it omit it."
459
462
  }
460
463
  }
464
+ },
465
+ "Profile": {
466
+ "type": "object",
467
+ "additionalProperties": false,
468
+ "description": "What each route normally does, sent once a minute rather than with every interval: a profile changes when the code changes, and at the aggregates' cadence it would not fit in the project's row budget (ADR 0017).",
469
+ "required": [
470
+ "start",
471
+ "durationMs",
472
+ "endpoints"
473
+ ],
474
+ "properties": {
475
+ "start": {
476
+ "type": "integer",
477
+ "minimum": 0,
478
+ "description": "Profile window start, Unix epoch milliseconds."
479
+ },
480
+ "durationMs": {
481
+ "type": "integer",
482
+ "minimum": 1
483
+ },
484
+ "endpoints": {
485
+ "type": "array",
486
+ "maxItems": 4096,
487
+ "items": { "$ref": "#/$defs/ProfileEndpoint" }
488
+ }
489
+ }
490
+ },
491
+ "ProfileEndpoint": {
492
+ "type": "object",
493
+ "additionalProperties": false,
494
+ "required": [
495
+ "method",
496
+ "route",
497
+ "operations"
498
+ ],
499
+ "properties": {
500
+ "method": { "enum": [
501
+ "GET",
502
+ "POST",
503
+ "PUT",
504
+ "PATCH",
505
+ "DELETE",
506
+ "HEAD",
507
+ "OPTIONS",
508
+ "OTHER"
509
+ ] },
510
+ "route": {
511
+ "type": "string",
512
+ "minLength": 1,
513
+ "maxLength": 256,
514
+ "description": "Normalised route template, e.g. /products/:id, or (other)."
515
+ },
516
+ "operations": {
517
+ "type": "array",
518
+ "maxItems": 64,
519
+ "items": { "$ref": "#/$defs/Operation" }
520
+ }
521
+ }
522
+ },
523
+ "Operation": {
524
+ "type": "object",
525
+ "additionalProperties": false,
526
+ "description": "One thing this route does, identified by a hash and labelled by an optional normalised text. The hash is the identity, so suppressing the text keeps the analysis whole (ADR 0017, invariant 5).",
527
+ "required": [
528
+ "kind",
529
+ "hash",
530
+ "count",
531
+ "totalMs"
532
+ ],
533
+ "properties": {
534
+ "kind": {
535
+ "enum": ["query", "error"],
536
+ "description": "What kind of operation this is."
537
+ },
538
+ "hash": {
539
+ "type": "string",
540
+ "minLength": 1,
541
+ "maxLength": 64,
542
+ "description": "Stable identity of the operation, or `(other)` for the bucket that holds the rest."
543
+ },
544
+ "text": {
545
+ "type": "string",
546
+ "maxLength": 1024,
547
+ "description": "Normalised text, e.g. `SELECT id FROM products WHERE id = ?`. Optional: a user who would rather not send it keeps the whole analysis, only without the label."
548
+ },
549
+ "count": {
550
+ "type": "integer",
551
+ "minimum": 0,
552
+ "description": "Executions across every request on this route."
553
+ },
554
+ "totalMs": {
555
+ "type": "number",
556
+ "minimum": 0,
557
+ "description": "Sum of their durations, in milliseconds."
558
+ },
559
+ "errors": {
560
+ "type": "integer",
561
+ "minimum": 0,
562
+ "description": "Executions that failed."
563
+ },
564
+ "distinct": {
565
+ "type": "integer",
566
+ "minimum": 0,
567
+ "description": "Only on the `(other)` bucket: how many distinct operations it merges, so a cap does not become a lie by omission."
568
+ }
569
+ }
461
570
  }
462
571
  }
463
572
  };
@@ -515,14 +624,24 @@ const CALLS_PER_REQUEST_BOUNDARIES_V0 = [
515
624
  /** Number of calls-per-request buckets: boundaries + 1 open-ended bucket. */
516
625
  const CALLS_PER_REQUEST_BUCKETS_V0 = 8;
517
626
  //#endregion
518
- //#region src/index.ts
519
- /**
520
- * Version of the ingestion protocol this package speaks. Every published minor of v0 stays acceptable to the
521
- * cloud, so an agent on an older minor keeps working: fields are only ever added, and always optional (ADR 0008).
522
- */
523
- const PROTOCOL_VERSION = "0.5.0";
627
+ //#region src/generated/paths.ts
524
628
  /** Path, relative to the ingest URL, that receives AggregatesBatch payloads. */
525
629
  const AGGREGATES_PATH = "/v0/aggregates";
630
+ //#endregion
631
+ //#region src/generated/versions.ts
632
+ /** Every published minor of v0, oldest first. The cloud never stops accepting one it published (ADR 0008). */
633
+ const ACCEPTED_PROTOCOL_VERSIONS_V0 = [
634
+ "0.1.0",
635
+ "0.2.0",
636
+ "0.3.0",
637
+ "0.4.0",
638
+ "0.5.0",
639
+ "0.6.0"
640
+ ];
641
+ /** The newest published minor: what an up-to-date agent stamps on every batch. */
642
+ const PROTOCOL_VERSION = "0.6.0";
643
+ //#endregion
644
+ //#region src/index.ts
526
645
  /** The JSON Schema (draft 2020-12) for AggregatesBatch, for validators on either side. */
527
646
  const AGGREGATES_SCHEMA_V0 = aggregates_schema_default;
528
647
  /** Index of the bucket a latency (ms) falls into: first boundary >= latency, else the open-ended last bucket. */
@@ -544,4 +663,4 @@ function callsPerRequestBucket(calls) {
544
663
  return n;
545
664
  }
546
665
  //#endregion
547
- export { AGGREGATES_PATH, AGGREGATES_SCHEMA_V0, CALLS_PER_REQUEST_BOUNDARIES_V0, CALLS_PER_REQUEST_BUCKETS_V0, LATENCY_BOUNDARIES_V0, LATENCY_BUCKETS_V0, PROTOCOL_VERSION, callsPerRequestBucket, latencyBucket };
666
+ export { ACCEPTED_PROTOCOL_VERSIONS_V0, AGGREGATES_PATH, AGGREGATES_SCHEMA_V0, CALLS_PER_REQUEST_BOUNDARIES_V0, CALLS_PER_REQUEST_BUCKETS_V0, LATENCY_BOUNDARIES_V0, LATENCY_BUCKETS_V0, PROTOCOL_VERSION, callsPerRequestBucket, latencyBucket };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@downtrace/protocol",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Downtrace ingestion protocol: JSON Schema, fixtures and generated TypeScript types",
5
5
  "type": "module",
6
6
  "exports": {
@@ -3,13 +3,14 @@
3
3
  "$id": "https://downtrace.io/schema/v0/aggregates.schema.json",
4
4
  "title": "AggregatesBatch",
5
5
  "description": "A batch of per-interval, per-route aggregates sent by an agent to the cloud (POST /v0/aggregates).",
6
+ "x-ingest-path": "/v0/aggregates",
6
7
  "type": "object",
7
8
  "additionalProperties": false,
8
9
  "required": ["protocol", "agent", "instance", "deploy", "intervals"],
9
10
  "properties": {
10
11
  "protocol": {
11
12
  "type": "string",
12
- "enum": ["0.1.0", "0.2.0", "0.3.0", "0.4.0", "0.5.0"],
13
+ "enum": ["0.1.0", "0.2.0", "0.3.0", "0.4.0", "0.5.0", "0.6.0"],
13
14
  "description": "Protocol version this batch conforms to. Every published minor of v0 stays acceptable."
14
15
  },
15
16
  "agent": {
@@ -28,6 +29,9 @@
28
29
  "items": {
29
30
  "$ref": "#/$defs/Interval"
30
31
  }
32
+ },
33
+ "profile": {
34
+ "$ref": "#/$defs/Profile"
31
35
  }
32
36
  },
33
37
  "$defs": {
@@ -364,6 +368,96 @@
364
368
  "description": "Time requests spent waiting to be able to talk to this dependency, on top of the time spent talking to it: a saturated connection pool shows up here and nowhere else. Optional; agents that cannot see it omit it."
365
369
  }
366
370
  }
371
+ },
372
+ "Profile": {
373
+ "type": "object",
374
+ "additionalProperties": false,
375
+ "description": "What each route normally does, sent once a minute rather than with every interval: a profile changes when the code changes, and at the aggregates' cadence it would not fit in the project's row budget (ADR 0017).",
376
+ "required": ["start", "durationMs", "endpoints"],
377
+ "properties": {
378
+ "start": {
379
+ "type": "integer",
380
+ "minimum": 0,
381
+ "description": "Profile window start, Unix epoch milliseconds."
382
+ },
383
+ "durationMs": {
384
+ "type": "integer",
385
+ "minimum": 1
386
+ },
387
+ "endpoints": {
388
+ "type": "array",
389
+ "maxItems": 4096,
390
+ "items": {
391
+ "$ref": "#/$defs/ProfileEndpoint"
392
+ }
393
+ }
394
+ }
395
+ },
396
+ "ProfileEndpoint": {
397
+ "type": "object",
398
+ "additionalProperties": false,
399
+ "required": ["method", "route", "operations"],
400
+ "properties": {
401
+ "method": {
402
+ "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "OTHER"]
403
+ },
404
+ "route": {
405
+ "type": "string",
406
+ "minLength": 1,
407
+ "maxLength": 256,
408
+ "description": "Normalised route template, e.g. /products/:id, or (other)."
409
+ },
410
+ "operations": {
411
+ "type": "array",
412
+ "maxItems": 64,
413
+ "items": {
414
+ "$ref": "#/$defs/Operation"
415
+ }
416
+ }
417
+ }
418
+ },
419
+ "Operation": {
420
+ "type": "object",
421
+ "additionalProperties": false,
422
+ "description": "One thing this route does, identified by a hash and labelled by an optional normalised text. The hash is the identity, so suppressing the text keeps the analysis whole (ADR 0017, invariant 5).",
423
+ "required": ["kind", "hash", "count", "totalMs"],
424
+ "properties": {
425
+ "kind": {
426
+ "enum": ["query", "error"],
427
+ "description": "What kind of operation this is."
428
+ },
429
+ "hash": {
430
+ "type": "string",
431
+ "minLength": 1,
432
+ "maxLength": 64,
433
+ "description": "Stable identity of the operation, or `(other)` for the bucket that holds the rest."
434
+ },
435
+ "text": {
436
+ "type": "string",
437
+ "maxLength": 1024,
438
+ "description": "Normalised text, e.g. `SELECT id FROM products WHERE id = ?`. Optional: a user who would rather not send it keeps the whole analysis, only without the label."
439
+ },
440
+ "count": {
441
+ "type": "integer",
442
+ "minimum": 0,
443
+ "description": "Executions across every request on this route."
444
+ },
445
+ "totalMs": {
446
+ "type": "number",
447
+ "minimum": 0,
448
+ "description": "Sum of their durations, in milliseconds."
449
+ },
450
+ "errors": {
451
+ "type": "integer",
452
+ "minimum": 0,
453
+ "description": "Executions that failed."
454
+ },
455
+ "distinct": {
456
+ "type": "integer",
457
+ "minimum": 0,
458
+ "description": "Only on the `(other)` bucket: how many distinct operations it merges, so a cap does not become a lie by omission."
459
+ }
460
+ }
367
461
  }
368
462
  }
369
463
  }
@@ -0,0 +1,64 @@
1
+ {
2
+ "protocol": "0.6.0",
3
+ "agent": {
4
+ "name": "@downtrace/agent",
5
+ "version": "0.0.0",
6
+ "runtime": "node",
7
+ "runtimeVersion": "v24.0.0"
8
+ },
9
+ "instance": {
10
+ "id": "7d1e9c6a-4f2b-4c1e-9b0a-2f6d3e8a1c55",
11
+ "hostname": "web-1",
12
+ "pid": 4242
13
+ },
14
+ "deploy": {
15
+ "version": "8f71ac2",
16
+ "environment": "production"
17
+ },
18
+ "intervals": [
19
+ {
20
+ "start": 1756900000000,
21
+ "durationMs": 10000,
22
+ "endpoints": [
23
+ {
24
+ "method": "POST",
25
+ "route": "/checkout",
26
+ "count": 3,
27
+ "errors": 0,
28
+ "status": {
29
+ "success": 3,
30
+ "redirect": 0,
31
+ "clientError": 0,
32
+ "serverError": 0
33
+ },
34
+ "latency": {
35
+ "counts": [
36
+ 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
37
+ ],
38
+ "sum": 2.4,
39
+ "max": 0.9
40
+ }
41
+ }
42
+ ]
43
+ }
44
+ ],
45
+ "profile": {
46
+ "start": 1756900000000,
47
+ "durationMs": 60000,
48
+ "endpoints": [
49
+ {
50
+ "method": "POST",
51
+ "route": "/checkout",
52
+ "operations": [
53
+ {
54
+ "kind": "query",
55
+ "text": "SELECT id, price_cents FROM products WHERE id = ?",
56
+ "count": 1440,
57
+ "totalMs": 812.4,
58
+ "errors": 0
59
+ }
60
+ ]
61
+ }
62
+ ]
63
+ }
64
+ }
@@ -0,0 +1,86 @@
1
+ {
2
+ "protocol": "0.6.0",
3
+ "agent": {
4
+ "name": "@downtrace/agent",
5
+ "version": "0.0.0",
6
+ "runtime": "node",
7
+ "runtimeVersion": "v24.0.0"
8
+ },
9
+ "instance": {
10
+ "id": "7d1e9c6a-4f2b-4c1e-9b0a-2f6d3e8a1c55",
11
+ "hostname": "web-1",
12
+ "pid": 4242
13
+ },
14
+ "deploy": {
15
+ "version": "8f71ac2",
16
+ "environment": "production"
17
+ },
18
+ "intervals": [
19
+ {
20
+ "start": 1756900000000,
21
+ "durationMs": 10000,
22
+ "endpoints": [
23
+ {
24
+ "method": "POST",
25
+ "route": "/checkout",
26
+ "count": 3,
27
+ "errors": 0,
28
+ "status": {
29
+ "success": 3,
30
+ "redirect": 0,
31
+ "clientError": 0,
32
+ "serverError": 0
33
+ },
34
+ "latency": {
35
+ "counts": [
36
+ 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
37
+ ],
38
+ "sum": 2.4,
39
+ "max": 0.9
40
+ }
41
+ }
42
+ ]
43
+ }
44
+ ],
45
+ "profile": {
46
+ "start": 1756900000000,
47
+ "durationMs": 60000,
48
+ "endpoints": [
49
+ {
50
+ "method": "POST",
51
+ "route": "/checkout",
52
+ "operations": [
53
+ {
54
+ "kind": "query",
55
+ "hash": "b7c1d0f2a3e45861",
56
+ "text": "SELECT id, price_cents FROM products WHERE id = ?",
57
+ "count": 1440,
58
+ "totalMs": 812.4,
59
+ "errors": 0
60
+ },
61
+ {
62
+ "kind": "query",
63
+ "hash": "9a02ff31b4c7d5e0",
64
+ "count": 720,
65
+ "totalMs": 190.1
66
+ },
67
+ {
68
+ "kind": "error",
69
+ "hash": "c4d5e6f708192a3b",
70
+ "text": "PoolTimeoutError",
71
+ "count": 3,
72
+ "totalMs": 15003.2,
73
+ "errors": 3
74
+ },
75
+ {
76
+ "kind": "query",
77
+ "hash": "(other)",
78
+ "count": 96,
79
+ "totalMs": 41.7,
80
+ "distinct": 7
81
+ }
82
+ ]
83
+ }
84
+ ]
85
+ }
86
+ }