@downtrace/agent 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/dist/{config-IDWW6i_8.js → config-K6GUM60m.js} +160 -24
- package/dist/index.d.ts +12 -2
- package/dist/index.js +1 -1
- package/dist/register.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -20,21 +20,35 @@ NODE_OPTIONS="--import @downtrace/agent/register" node server.js
|
|
|
20
20
|
| Variable | Required | What it is |
|
|
21
21
|
|---|---|---|
|
|
22
22
|
| `DOWNTRACE_TOKEN` | yes | The project's ingest token |
|
|
23
|
-
| `DOWNTRACE_URL` | yes | Base URL of the cloud
|
|
23
|
+
| `DOWNTRACE_URL` | yes | Base URL of the cloud, `http://` or `https://`; trailing slashes are ignored |
|
|
24
24
|
| `DOWNTRACE_ENV` | no | Environment; falls back to `NODE_ENV`, then `production` |
|
|
25
25
|
| `DOWNTRACE_VERSION` | no | Deployed version or commit; detected from `APP_VERSION`, `GIT_SHA`, `VERCEL_GIT_COMMIT_SHA`, `HEROKU_SLUG_COMMIT`, `SOURCE_VERSION`, `RENDER_GIT_COMMIT`, `RAILWAY_GIT_COMMIT_SHA`; else `unknown` |
|
|
26
|
-
| `DOWNTRACE_DEBUG` | no | `1` to log the agent's own activity to stderr |
|
|
27
|
-
| `DOWNTRACE_INTERVAL_MS` | no | Aggregation interval (min 1000; default 10000) |
|
|
26
|
+
| `DOWNTRACE_DEBUG` | no | `1` or `true` to log the agent's own activity to stderr |
|
|
27
|
+
| `DOWNTRACE_INTERVAL_MS` | no | Aggregation interval in ms (min 1000; default 10000; anything else falls back to the default) |
|
|
28
|
+
| `DOWNTRACE_INSTRUMENT` | no | `none` stops the agent from observing database drivers; anything else keeps it on |
|
|
28
29
|
|
|
29
|
-
Without `DOWNTRACE_TOKEN` and `DOWNTRACE_URL` the agent prints one warning and does nothing else.
|
|
30
|
+
Without `DOWNTRACE_TOKEN` and `DOWNTRACE_URL` (or with a `DOWNTRACE_URL` that is not `http(s)://`) the agent prints one warning and does nothing else.
|
|
30
31
|
|
|
31
32
|
## What leaves your server
|
|
32
33
|
|
|
33
34
|
Only structural metadata: method, **route template** (`/products/:id`, never the actual URL), status, counts and a fixed-bucket latency histogram per route and interval, plus the process identity (random id, hostname, pid) and the deploy (version, environment). No bodies, no headers, no query strings. The exact contract is the JSON Schema in [`@downtrace/protocol`](https://www.npmjs.com/package/@downtrace/protocol).
|
|
34
35
|
|
|
36
|
+
### Database work per request
|
|
37
|
+
|
|
38
|
+
When your application uses `pg`, the agent also counts the queries each request makes, how long they took in total
|
|
39
|
+
and the slowest one, and reports that distribution per route. It is what turns "this endpoint got slower" into "this
|
|
40
|
+
endpoint went from 12 queries per request to 65". **It never reads the query text or its values**, only counts and
|
|
41
|
+
durations.
|
|
42
|
+
|
|
43
|
+
The agent loads before your application (`node --import`), so it wraps the driver before you import it and you write
|
|
44
|
+
no code. The wrapper passes arguments, results and errors through untouched, and a failure inside it runs your query
|
|
45
|
+
anyway. `DOWNTRACE_INSTRUMENT=none` turns it off.
|
|
46
|
+
|
|
35
47
|
## Guarantees
|
|
36
48
|
|
|
37
|
-
-
|
|
49
|
+
- HTTP requests are observed through Node's `diagnostics_channel`, without touching your code. To count queries per
|
|
50
|
+
request the agent does wrap one method, `pg`'s `Client.prototype.query`: it passes arguments, results and errors
|
|
51
|
+
through untouched, and if the wrapper itself fails your query still runs. `DOWNTRACE_INSTRUMENT=none` disables it.
|
|
38
52
|
- Sending is asynchronous with `fetch`, off the request path; a bounded queue of 6 intervals — if the cloud is unreachable, the oldest is dropped.
|
|
39
53
|
- Every hook is guarded; after 10 internal errors the agent disables itself and says so once.
|
|
40
54
|
- At most 500 distinct routes per interval; the rest fold into `(other)`.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import diagnostics_channel from "node:diagnostics_channel";
|
|
3
4
|
import { hostname } from "node:os";
|
|
4
|
-
import { AGGREGATES_PATH, LATENCY_BUCKETS_V0, PROTOCOL_VERSION, latencyBucket } from "@downtrace/protocol";
|
|
5
|
+
import { AGGREGATES_PATH, LATENCY_BUCKETS_V0, PROTOCOL_VERSION, QUERIES_PER_REQUEST_BUCKETS_V0, latencyBucket, queriesPerRequestBucket } from "@downtrace/protocol";
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
import { performance as performance$1 } from "node:perf_hooks";
|
|
5
8
|
//#region src/routes.ts
|
|
6
9
|
const METHODS = /* @__PURE__ */ new Set([
|
|
7
10
|
"GET",
|
|
@@ -64,7 +67,7 @@ var IntervalAggregator = class {
|
|
|
64
67
|
get size() {
|
|
65
68
|
return this.endpoints.size;
|
|
66
69
|
}
|
|
67
|
-
record(method, route, status, ms) {
|
|
70
|
+
record(method, route, status, ms, work) {
|
|
68
71
|
let key = `${method} ${route}`;
|
|
69
72
|
let acc = this.endpoints.get(key);
|
|
70
73
|
if (!acc) {
|
|
@@ -85,7 +88,8 @@ var IntervalAggregator = class {
|
|
|
85
88
|
serverError: 0,
|
|
86
89
|
counts: new Uint32Array(LATENCY_BUCKETS_V0),
|
|
87
90
|
sum: 0,
|
|
88
|
-
max: 0
|
|
91
|
+
max: 0,
|
|
92
|
+
pg: void 0
|
|
89
93
|
};
|
|
90
94
|
this.endpoints.set(key, acc);
|
|
91
95
|
}
|
|
@@ -102,6 +106,17 @@ var IntervalAggregator = class {
|
|
|
102
106
|
acc.counts[bucket] = (acc.counts[bucket] ?? 0) + 1;
|
|
103
107
|
acc.sum += latency;
|
|
104
108
|
if (latency > acc.max) acc.max = latency;
|
|
109
|
+
if (work) {
|
|
110
|
+
acc.pg ??= {
|
|
111
|
+
counts: new Uint32Array(QUERIES_PER_REQUEST_BUCKETS_V0),
|
|
112
|
+
sum: 0,
|
|
113
|
+
max: 0
|
|
114
|
+
};
|
|
115
|
+
const bucket = queriesPerRequestBucket(work.queries);
|
|
116
|
+
acc.pg.counts[bucket] = (acc.pg.counts[bucket] ?? 0) + 1;
|
|
117
|
+
acc.pg.sum += work.queryMs;
|
|
118
|
+
if (work.queryMaxMs > acc.pg.max) acc.pg.max = work.queryMaxMs;
|
|
119
|
+
}
|
|
105
120
|
}
|
|
106
121
|
/** Closes the current interval and starts a new one. Returns null when nothing was recorded. */
|
|
107
122
|
rotate() {
|
|
@@ -113,23 +128,31 @@ var IntervalAggregator = class {
|
|
|
113
128
|
this.start = now;
|
|
114
129
|
if (endpoints.size === 0) return null;
|
|
115
130
|
const out = [];
|
|
116
|
-
for (const acc of endpoints.values())
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
for (const acc of endpoints.values()) {
|
|
132
|
+
const postgres = acc.pg ? {
|
|
133
|
+
queriesPerRequest: Array.from(acc.pg.counts),
|
|
134
|
+
totalMs: round3(acc.pg.sum),
|
|
135
|
+
max: round3(acc.pg.max)
|
|
136
|
+
} : void 0;
|
|
137
|
+
out.push({
|
|
138
|
+
method: acc.method,
|
|
139
|
+
route: acc.route,
|
|
140
|
+
count: acc.count,
|
|
141
|
+
errors: acc.errors,
|
|
142
|
+
status: {
|
|
143
|
+
success: acc.success,
|
|
144
|
+
redirect: acc.redirect,
|
|
145
|
+
clientError: acc.clientError,
|
|
146
|
+
serverError: acc.serverError
|
|
147
|
+
},
|
|
148
|
+
latency: {
|
|
149
|
+
counts: Array.from(acc.counts),
|
|
150
|
+
sum: round3(acc.sum),
|
|
151
|
+
max: round3(acc.max)
|
|
152
|
+
},
|
|
153
|
+
...postgres ? { postgres } : {}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
133
156
|
return {
|
|
134
157
|
start: Math.floor(start),
|
|
135
158
|
durationMs: Math.max(1, Math.round(now - start)),
|
|
@@ -141,6 +164,107 @@ function round3(n) {
|
|
|
141
164
|
return Math.round(n * 1e3) / 1e3;
|
|
142
165
|
}
|
|
143
166
|
//#endregion
|
|
167
|
+
//#region src/context.ts
|
|
168
|
+
const storage = new AsyncLocalStorage();
|
|
169
|
+
/**
|
|
170
|
+
* Opens the context for a request. Called from the `http.server.request.start` subscriber, which Node publishes
|
|
171
|
+
* inside the request's own async context, so `enterWith` reaches the handler and everything it awaits. Verified
|
|
172
|
+
* against concurrent keep-alive traffic: each request counts its own work.
|
|
173
|
+
*/
|
|
174
|
+
function enterRequest() {
|
|
175
|
+
const ctx = {
|
|
176
|
+
queries: 0,
|
|
177
|
+
queryMs: 0,
|
|
178
|
+
queryMaxMs: 0
|
|
179
|
+
};
|
|
180
|
+
storage.enterWith(ctx);
|
|
181
|
+
return ctx;
|
|
182
|
+
}
|
|
183
|
+
/** Records one finished query against the current request. Work outside a request is not attributed to any route. */
|
|
184
|
+
function recordQuery(ms) {
|
|
185
|
+
const ctx = storage.getStore();
|
|
186
|
+
if (!ctx) return;
|
|
187
|
+
ctx.queries += 1;
|
|
188
|
+
ctx.queryMs += ms;
|
|
189
|
+
if (ms > ctx.queryMaxMs) ctx.queryMaxMs = ms;
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/instrument/pg.ts
|
|
193
|
+
const MARK = Symbol.for("downtrace.pg.instrumented");
|
|
194
|
+
/**
|
|
195
|
+
* Wraps `pg`'s `Client.prototype.query` so every query counts towards the request that issued it.
|
|
196
|
+
*
|
|
197
|
+
* The agent loads before the application (`node --import`), resolves `pg` from the application's own root and
|
|
198
|
+
* patches the prototype. CommonJS modules are cached by resolved path, so the instance the application later
|
|
199
|
+
* imports is the one patched here: no loader hooks, no dependency, and it works whether the app is ESM or CJS.
|
|
200
|
+
*
|
|
201
|
+
* Returns the instrumented module's version, or undefined when there is nothing to instrument.
|
|
202
|
+
*/
|
|
203
|
+
function instrumentPg(deps) {
|
|
204
|
+
let pg;
|
|
205
|
+
let version = "unknown";
|
|
206
|
+
try {
|
|
207
|
+
if (deps.moduleImpl !== void 0) pg = deps.moduleImpl;
|
|
208
|
+
else {
|
|
209
|
+
const base = deps.from ?? process.argv[1] ?? `${process.cwd()}/`;
|
|
210
|
+
const require = createRequire(base);
|
|
211
|
+
pg = require("pg");
|
|
212
|
+
const pkg = require("pg/package.json");
|
|
213
|
+
if (typeof pkg.version === "string") version = pkg.version;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const proto = pg.Client?.prototype;
|
|
219
|
+
if (!proto || typeof proto.query !== "function") {
|
|
220
|
+
deps.log.debug("pg found but Client.prototype.query is not a function; not instrumenting");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (proto[MARK] === true) return version;
|
|
224
|
+
const original = proto.query;
|
|
225
|
+
const wrapped = function(...args) {
|
|
226
|
+
let done;
|
|
227
|
+
try {
|
|
228
|
+
const started = performance$1.now();
|
|
229
|
+
let counted = false;
|
|
230
|
+
done = () => {
|
|
231
|
+
if (counted) return;
|
|
232
|
+
counted = true;
|
|
233
|
+
recordQuery(performance$1.now() - started);
|
|
234
|
+
};
|
|
235
|
+
const last = args.at(-1);
|
|
236
|
+
if (typeof last === "function") {
|
|
237
|
+
const callback = last;
|
|
238
|
+
const finish = done;
|
|
239
|
+
args[args.length - 1] = function(...cbArgs) {
|
|
240
|
+
finish();
|
|
241
|
+
return callback.apply(this, cbArgs);
|
|
242
|
+
};
|
|
243
|
+
return original.apply(this, args);
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
return original.apply(this, args);
|
|
247
|
+
}
|
|
248
|
+
const result = original.apply(this, args);
|
|
249
|
+
if (result && typeof result.then === "function") {
|
|
250
|
+
const settle = done;
|
|
251
|
+
return result.then((value) => {
|
|
252
|
+
settle?.();
|
|
253
|
+
return value;
|
|
254
|
+
}, (err) => {
|
|
255
|
+
settle?.();
|
|
256
|
+
throw err;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
done?.();
|
|
260
|
+
return result;
|
|
261
|
+
};
|
|
262
|
+
proto.query = wrapped;
|
|
263
|
+
proto[MARK] = true;
|
|
264
|
+
deps.log.debug(`instrumented pg ${version}`);
|
|
265
|
+
return version;
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
144
268
|
//#region src/log.ts
|
|
145
269
|
const PREFIX = "[downtrace]";
|
|
146
270
|
/** Minimal stderr logger. `debug` lines only appear with DOWNTRACE_DEBUG. */
|
|
@@ -236,7 +360,7 @@ var Sender = class {
|
|
|
236
360
|
//#endregion
|
|
237
361
|
//#region src/version.ts
|
|
238
362
|
/** Version of this agent build, from package.json. */
|
|
239
|
-
const AGENT_VERSION = "0.
|
|
363
|
+
const AGENT_VERSION = "0.2.0";
|
|
240
364
|
//#endregion
|
|
241
365
|
//#region src/agent.ts
|
|
242
366
|
const REQUEST_START = "http.server.request.start";
|
|
@@ -258,6 +382,8 @@ var Agent = class {
|
|
|
258
382
|
sender;
|
|
259
383
|
handleSignals;
|
|
260
384
|
starts = /* @__PURE__ */ new WeakMap();
|
|
385
|
+
contexts = /* @__PURE__ */ new WeakMap();
|
|
386
|
+
instrumented = false;
|
|
261
387
|
timer;
|
|
262
388
|
started = false;
|
|
263
389
|
recorded = 0;
|
|
@@ -317,6 +443,11 @@ var Agent = class {
|
|
|
317
443
|
start() {
|
|
318
444
|
if (this.started || this.disabled) return;
|
|
319
445
|
this.started = true;
|
|
446
|
+
if (this.config.instrument) {
|
|
447
|
+
const pg = instrumentPg({ log: this.log });
|
|
448
|
+
this.instrumented = pg !== void 0;
|
|
449
|
+
if (pg) this.log.debug(`instrumented pg ${pg}`);
|
|
450
|
+
}
|
|
320
451
|
diagnostics_channel.subscribe(REQUEST_START, this.onStart);
|
|
321
452
|
diagnostics_channel.subscribe(RESPONSE_FINISH, this.onFinish);
|
|
322
453
|
this.timer = setInterval(() => void this.flushNow(), this.config.intervalMs);
|
|
@@ -349,7 +480,9 @@ var Agent = class {
|
|
|
349
480
|
}
|
|
350
481
|
requestStarted(message) {
|
|
351
482
|
const request = message.request;
|
|
352
|
-
if (request)
|
|
483
|
+
if (!request) return;
|
|
484
|
+
this.starts.set(request, performance.now());
|
|
485
|
+
if (this.instrumented) this.contexts.set(request, enterRequest());
|
|
353
486
|
}
|
|
354
487
|
responseFinished(message) {
|
|
355
488
|
const { request, response } = message;
|
|
@@ -357,7 +490,9 @@ var Agent = class {
|
|
|
357
490
|
const startedAt = this.starts.get(request);
|
|
358
491
|
this.starts.delete(request);
|
|
359
492
|
const ms = startedAt === void 0 ? 0 : performance.now() - startedAt;
|
|
360
|
-
this.
|
|
493
|
+
const work = this.contexts.get(request);
|
|
494
|
+
this.contexts.delete(request);
|
|
495
|
+
this.recorder.record(normalizeMethod(request.method), routeOf(request), response?.statusCode ?? 0, ms, work);
|
|
361
496
|
this.recorded += 1;
|
|
362
497
|
}
|
|
363
498
|
/** Every hook runs through here: an agent bug must never reach the application. */
|
|
@@ -439,7 +574,8 @@ function configFromEnv(env = process.env) {
|
|
|
439
574
|
environment: clamp(env.DOWNTRACE_ENV ?? env.NODE_ENV ?? "production", 64),
|
|
440
575
|
version: detectVersion(env),
|
|
441
576
|
debug: env.DOWNTRACE_DEBUG === "1" || env.DOWNTRACE_DEBUG === "true",
|
|
442
|
-
intervalMs: Number.isInteger(interval) && interval >= MIN_INTERVAL_MS ? interval : DEFAULT_INTERVAL_MS
|
|
577
|
+
intervalMs: Number.isInteger(interval) && interval >= MIN_INTERVAL_MS ? interval : DEFAULT_INTERVAL_MS,
|
|
578
|
+
instrument: (env.DOWNTRACE_INSTRUMENT ?? "auto") !== "none"
|
|
443
579
|
}
|
|
444
580
|
};
|
|
445
581
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,8 +19,14 @@ declare function routeOf(req: RouteSource): string;
|
|
|
19
19
|
declare function heuristicTemplate(url: string): string;
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region src/aggregator.d.ts
|
|
22
|
+
/** What one finished request did in Postgres, as seen by the instrumented driver. */
|
|
23
|
+
interface QueryWork {
|
|
24
|
+
queries: number;
|
|
25
|
+
queryMs: number;
|
|
26
|
+
queryMaxMs: number;
|
|
27
|
+
}
|
|
22
28
|
interface Recorder {
|
|
23
|
-
record(method: Method, route: string, status: number, ms: number): void;
|
|
29
|
+
record(method: Method, route: string, status: number, ms: number, work?: QueryWork | undefined): void;
|
|
24
30
|
rotate(): Interval | null;
|
|
25
31
|
}
|
|
26
32
|
declare const DEFAULT_MAX_ROUTES = 500;
|
|
@@ -36,7 +42,7 @@ declare class IntervalAggregator implements Recorder {
|
|
|
36
42
|
private readonly now;
|
|
37
43
|
constructor(maxRoutes?: number, now?: () => number);
|
|
38
44
|
get size(): number;
|
|
39
|
-
record(method: Method, route: string, status: number, ms: number): void;
|
|
45
|
+
record(method: Method, route: string, status: number, ms: number, work?: QueryWork | undefined): void;
|
|
40
46
|
/** Closes the current interval and starts a new one. Returns null when nothing was recorded. */
|
|
41
47
|
rotate(): Interval | null;
|
|
42
48
|
}
|
|
@@ -51,6 +57,8 @@ interface AgentConfig {
|
|
|
51
57
|
debug: boolean;
|
|
52
58
|
/** Aggregation interval; 10 s in production. */
|
|
53
59
|
intervalMs: number;
|
|
60
|
+
/** Observe database drivers to attribute work to each request. `DOWNTRACE_INSTRUMENT=none` turns it off. */
|
|
61
|
+
instrument: boolean;
|
|
54
62
|
}
|
|
55
63
|
type ConfigResult = {
|
|
56
64
|
ok: true;
|
|
@@ -139,6 +147,8 @@ declare class Agent {
|
|
|
139
147
|
private readonly sender;
|
|
140
148
|
private readonly handleSignals;
|
|
141
149
|
private readonly starts;
|
|
150
|
+
private readonly contexts;
|
|
151
|
+
private instrumented;
|
|
142
152
|
private timer;
|
|
143
153
|
private started;
|
|
144
154
|
private recorded;
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createAgent, c as Sender, d as IntervalAggregator, f as OTHER_ROUTE, h as routeOf, i as Agent, l as createLogger, m as normalizeMethod, n as configFromEnv, o as AGENT_VERSION, p as heuristicTemplate, r as detectVersion, s as DEFAULT_MAX_QUEUED, t as DEFAULT_INTERVAL_MS, u as DEFAULT_MAX_ROUTES } from "./config-
|
|
1
|
+
import { a as createAgent, c as Sender, d as IntervalAggregator, f as OTHER_ROUTE, h as routeOf, i as Agent, l as createLogger, m as normalizeMethod, n as configFromEnv, o as AGENT_VERSION, p as heuristicTemplate, r as detectVersion, s as DEFAULT_MAX_QUEUED, t as DEFAULT_INTERVAL_MS, u as DEFAULT_MAX_ROUTES } from "./config-K6GUM60m.js";
|
|
2
2
|
export { AGENT_VERSION, Agent, DEFAULT_INTERVAL_MS, DEFAULT_MAX_QUEUED, DEFAULT_MAX_ROUTES, IntervalAggregator, OTHER_ROUTE, Sender, configFromEnv, createAgent, createLogger, detectVersion, heuristicTemplate, normalizeMethod, routeOf };
|
package/dist/register.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as createAgent, l as createLogger, n as configFromEnv } from "./config-
|
|
1
|
+
import { a as createAgent, l as createLogger, n as configFromEnv } from "./config-K6GUM60m.js";
|
|
2
2
|
//#region src/register.ts
|
|
3
3
|
/**
|
|
4
4
|
* Entry point users load with `node --import @downtrace/agent/register`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@downtrace/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Downtrace agent for Node.js: a flight recorder for your backend. Observes HTTP requests, aggregates locally, ships compact batches, never in your request path.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@downtrace/protocol": "0.
|
|
25
|
+
"@downtrace/protocol": "0.2.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/express": "^5.0.6",
|