@traffic-twin/sdk 0.0.0-stage → 0.1.1
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/LICENSE +202 -0
- package/README.md +171 -2
- package/dist/cjs/agent.d.ts +143 -0
- package/dist/cjs/agent.js +927 -0
- package/dist/cjs/capture/body.d.ts +11 -0
- package/dist/cjs/capture/body.js +73 -0
- package/dist/cjs/capture/common.d.ts +75 -0
- package/dist/cjs/capture/common.js +180 -0
- package/dist/cjs/capture/context.d.ts +28 -0
- package/dist/cjs/capture/context.js +92 -0
- package/dist/cjs/capture/inbound.d.ts +12 -0
- package/dist/cjs/capture/inbound.js +195 -0
- package/dist/cjs/capture/outbound-http.d.ts +4 -0
- package/dist/cjs/capture/outbound-http.js +200 -0
- package/dist/cjs/capture/outbound-undici.d.ts +2 -0
- package/dist/cjs/capture/outbound-undici.js +242 -0
- package/dist/cjs/capture/wire.d.ts +27 -0
- package/dist/cjs/capture/wire.js +68 -0
- package/dist/cjs/export/event.d.ts +46 -0
- package/dist/cjs/export/event.js +91 -0
- package/dist/cjs/export/exporter.d.ts +50 -0
- package/dist/cjs/export/exporter.js +192 -0
- package/dist/cjs/export/pipeline.d.ts +20 -0
- package/dist/cjs/export/pipeline.js +40 -0
- package/dist/cjs/fleet/asn1.d.ts +44 -0
- package/dist/cjs/fleet/asn1.js +150 -0
- package/dist/cjs/fleet/client.d.ts +84 -0
- package/dist/cjs/fleet/client.js +216 -0
- package/dist/cjs/fleet/http.d.ts +17 -0
- package/dist/cjs/fleet/http.js +80 -0
- package/dist/cjs/fleet/store.d.ts +18 -0
- package/dist/cjs/fleet/store.js +115 -0
- package/dist/cjs/index.d.ts +14 -0
- package/dist/cjs/index.js +69 -0
- package/dist/cjs/log.d.ts +4 -0
- package/dist/cjs/log.js +30 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/register.d.ts +1 -0
- package/dist/cjs/register.js +18 -0
- package/dist/cjs/sanitizer/bodies.d.ts +55 -0
- package/dist/cjs/sanitizer/bodies.js +710 -0
- package/dist/cjs/sanitizer/formats.d.ts +26 -0
- package/dist/cjs/sanitizer/formats.js +110 -0
- package/dist/cjs/sanitizer/graphql.d.ts +23 -0
- package/dist/cjs/sanitizer/graphql.js +478 -0
- package/dist/cjs/sanitizer/hostpat.d.ts +15 -0
- package/dist/cjs/sanitizer/hostpat.js +129 -0
- package/dist/cjs/sanitizer/json.d.ts +60 -0
- package/dist/cjs/sanitizer/json.js +460 -0
- package/dist/cjs/sanitizer/policy.d.ts +224 -0
- package/dist/cjs/sanitizer/policy.js +820 -0
- package/dist/cjs/sanitizer/sanitizer.d.ts +149 -0
- package/dist/cjs/sanitizer/sanitizer.js +1473 -0
- package/dist/cjs/sanitizer/utf8.d.ts +22 -0
- package/dist/cjs/sanitizer/utf8.js +180 -0
- package/dist/cjs/sanitizer/vectors.d.ts +22 -0
- package/dist/cjs/sanitizer/vectors.js +130 -0
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.js +5 -0
- package/dist/cjs/worker/dir.d.ts +1 -0
- package/dist/cjs/worker/dir.js +8 -0
- package/dist/cjs/worker/pool.d.ts +47 -0
- package/dist/cjs/worker/pool.js +179 -0
- package/dist/cjs/worker/protocol.d.ts +37 -0
- package/dist/cjs/worker/protocol.js +3 -0
- package/dist/cjs/worker/sanitizer-worker.d.ts +1 -0
- package/dist/cjs/worker/sanitizer-worker.js +53 -0
- package/dist/esm/agent.d.ts +143 -0
- package/dist/esm/agent.js +921 -0
- package/dist/esm/capture/body.d.ts +11 -0
- package/dist/esm/capture/body.js +69 -0
- package/dist/esm/capture/common.d.ts +75 -0
- package/dist/esm/capture/common.js +166 -0
- package/dist/esm/capture/context.d.ts +28 -0
- package/dist/esm/capture/context.js +84 -0
- package/dist/esm/capture/inbound.d.ts +12 -0
- package/dist/esm/capture/inbound.js +187 -0
- package/dist/esm/capture/outbound-http.d.ts +4 -0
- package/dist/esm/capture/outbound-http.js +193 -0
- package/dist/esm/capture/outbound-undici.d.ts +2 -0
- package/dist/esm/capture/outbound-undici.js +236 -0
- package/dist/esm/capture/wire.d.ts +27 -0
- package/dist/esm/capture/wire.js +65 -0
- package/dist/esm/export/event.d.ts +46 -0
- package/dist/esm/export/event.js +85 -0
- package/dist/esm/export/exporter.d.ts +50 -0
- package/dist/esm/export/exporter.js +188 -0
- package/dist/esm/export/pipeline.d.ts +20 -0
- package/dist/esm/export/pipeline.js +37 -0
- package/dist/esm/fleet/asn1.d.ts +44 -0
- package/dist/esm/fleet/asn1.js +128 -0
- package/dist/esm/fleet/client.d.ts +84 -0
- package/dist/esm/fleet/client.js +204 -0
- package/dist/esm/fleet/http.d.ts +17 -0
- package/dist/esm/fleet/http.js +73 -0
- package/dist/esm/fleet/store.d.ts +18 -0
- package/dist/esm/fleet/store.js +110 -0
- package/dist/esm/index.d.ts +14 -0
- package/dist/esm/index.js +48 -0
- package/dist/esm/log.d.ts +4 -0
- package/dist/esm/log.js +25 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/register.d.ts +1 -0
- package/dist/esm/register.js +16 -0
- package/dist/esm/sanitizer/bodies.d.ts +55 -0
- package/dist/esm/sanitizer/bodies.js +694 -0
- package/dist/esm/sanitizer/formats.d.ts +26 -0
- package/dist/esm/sanitizer/formats.js +101 -0
- package/dist/esm/sanitizer/graphql.d.ts +23 -0
- package/dist/esm/sanitizer/graphql.js +472 -0
- package/dist/esm/sanitizer/hostpat.d.ts +15 -0
- package/dist/esm/sanitizer/hostpat.js +122 -0
- package/dist/esm/sanitizer/json.d.ts +60 -0
- package/dist/esm/sanitizer/json.js +447 -0
- package/dist/esm/sanitizer/policy.d.ts +224 -0
- package/dist/esm/sanitizer/policy.js +800 -0
- package/dist/esm/sanitizer/sanitizer.d.ts +149 -0
- package/dist/esm/sanitizer/sanitizer.js +1459 -0
- package/dist/esm/sanitizer/utf8.d.ts +22 -0
- package/dist/esm/sanitizer/utf8.js +166 -0
- package/dist/esm/sanitizer/vectors.d.ts +22 -0
- package/dist/esm/sanitizer/vectors.js +125 -0
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +2 -0
- package/dist/esm/worker/dir.d.ts +1 -0
- package/dist/esm/worker/dir.js +5 -0
- package/dist/esm/worker/pool.d.ts +47 -0
- package/dist/esm/worker/pool.js +175 -0
- package/dist/esm/worker/protocol.d.ts +37 -0
- package/dist/esm/worker/protocol.js +2 -0
- package/dist/esm/worker/sanitizer-worker.d.ts +1 -0
- package/dist/esm/worker/sanitizer-worker.js +51 -0
- package/package.json +75 -4
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Outbound capture for node:http / node:https clients through diagnostics_channel
|
|
3
|
+
// (`http.client.request.start`, `http.client.response.finish`, `http.client.request.error`).
|
|
4
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
|
+
};
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.UNREAD_BODY_TIMEOUT_MS = void 0;
|
|
9
|
+
exports.installOutboundHttp = installOutboundHttp;
|
|
10
|
+
const node_diagnostics_channel_1 = __importDefault(require("node:diagnostics_channel"));
|
|
11
|
+
const body_js_1 = require("./body.js");
|
|
12
|
+
const common_js_1 = require("./common.js");
|
|
13
|
+
const context_js_1 = require("./context.js");
|
|
14
|
+
const inbound_js_1 = require("./inbound.js");
|
|
15
|
+
const EMPTY = Buffer.alloc(0);
|
|
16
|
+
/** How long an unread response body is waited for before the exchange is closed as not consumed. */
|
|
17
|
+
exports.UNREAD_BODY_TIMEOUT_MS = 60_000;
|
|
18
|
+
const kState = Symbol('traffic-twin.outbound');
|
|
19
|
+
const kSeen = Symbol('traffic-twin.outbound.seen');
|
|
20
|
+
function installOutboundHttp(host) {
|
|
21
|
+
// `http.client.request.created` (Node >= 22.12 / 23.2) fires in the ClientRequest
|
|
22
|
+
// constructor, before the app can write or end: body capture and the optional baggage
|
|
23
|
+
// member work there. On older runtimes `start` fires once a socket is assigned, which may
|
|
24
|
+
// be after end(): the body is then reported as unknown and nothing is injected.
|
|
25
|
+
const onCreated = (message) => {
|
|
26
|
+
if (common_js_1.internal.depth > 0)
|
|
27
|
+
return;
|
|
28
|
+
const t = (0, common_js_1.hrNow)();
|
|
29
|
+
try {
|
|
30
|
+
const request = message.request;
|
|
31
|
+
if (request[kSeen])
|
|
32
|
+
return;
|
|
33
|
+
request[kSeen] = true;
|
|
34
|
+
if (request.agent && common_js_1.internalAgents.has(request.agent))
|
|
35
|
+
return;
|
|
36
|
+
start(host, request, t);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
host.noteError('capture_outbound_start');
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const onStart = onCreated;
|
|
43
|
+
const onResponse = (message) => {
|
|
44
|
+
try {
|
|
45
|
+
const { request, response } = message;
|
|
46
|
+
const st = request[kState];
|
|
47
|
+
if (!st || st.done)
|
|
48
|
+
return;
|
|
49
|
+
st.response = response;
|
|
50
|
+
(0, inbound_js_1.tapPush)(response, st.resTap);
|
|
51
|
+
response.once('end', () => complete(host, request, true));
|
|
52
|
+
response.once('close', () => complete(host, request, false));
|
|
53
|
+
st.timer = setTimeout(() => complete(host, request, false), exports.UNREAD_BODY_TIMEOUT_MS);
|
|
54
|
+
st.timer.unref();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
host.noteError('capture_outbound_response');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const onError = (message) => {
|
|
61
|
+
try {
|
|
62
|
+
const { request, error } = message;
|
|
63
|
+
const st = request[kState];
|
|
64
|
+
if (st && !st.response)
|
|
65
|
+
st.netError = (0, common_js_1.netErrorOf)(error);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
node_diagnostics_channel_1.default.subscribe('http.client.request.created', onCreated);
|
|
72
|
+
node_diagnostics_channel_1.default.subscribe('http.client.request.start', onStart);
|
|
73
|
+
node_diagnostics_channel_1.default.subscribe('http.client.response.finish', onResponse);
|
|
74
|
+
node_diagnostics_channel_1.default.subscribe('http.client.request.error', onError);
|
|
75
|
+
return () => {
|
|
76
|
+
node_diagnostics_channel_1.default.unsubscribe('http.client.request.created', onCreated);
|
|
77
|
+
node_diagnostics_channel_1.default.unsubscribe('http.client.request.start', onStart);
|
|
78
|
+
node_diagnostics_channel_1.default.unsubscribe('http.client.response.finish', onResponse);
|
|
79
|
+
node_diagnostics_channel_1.default.unsubscribe('http.client.request.error', onError);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Authority when the request has no Host header: the host option, bracketed if it is an IPv6 literal. */
|
|
83
|
+
function fallbackAuthority(request) {
|
|
84
|
+
const h = String(request.host ?? '');
|
|
85
|
+
return h.includes(':') && !h.startsWith('[') ? '[' + h + ']' : h;
|
|
86
|
+
}
|
|
87
|
+
function start(host, request, t) {
|
|
88
|
+
const san = host.active();
|
|
89
|
+
if (!san)
|
|
90
|
+
return;
|
|
91
|
+
host.noteSeen();
|
|
92
|
+
const ctx = context_js_1.als.getStore();
|
|
93
|
+
if (ctx !== undefined && !ctx.sampled && !host.propagates()) {
|
|
94
|
+
// Inherited "not sampled" and nothing to inject: no work at all for this call.
|
|
95
|
+
host.noteOverhead((0, common_js_1.hrNow)() - t);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const hostHeader = request.getHeader('host');
|
|
99
|
+
const proto = typeof request.protocol === 'string' ? request.protocol : 'http:';
|
|
100
|
+
// The host as the client sees it (spec v1.5, point 9): the Host header as is, a trailing dot and
|
|
101
|
+
// an `xn--` form untouched, IPv6 with its brackets. No IDN conversion, no dot stripping.
|
|
102
|
+
const urlBase = proto + '//' + (typeof hostHeader === 'string' ? hostHeader : fallbackAuthority(request));
|
|
103
|
+
const tr = (0, common_js_1.outboundTrace)(ctx);
|
|
104
|
+
const decision = san.decide({ dir: 'outbound', req_headers: [], trace_id: tr.samplingTraceId, inherited: ctx?.sampled });
|
|
105
|
+
if (host.mayPropagate(urlBase) && !request.headersSent) {
|
|
106
|
+
try {
|
|
107
|
+
const prev = request.getHeader('baggage');
|
|
108
|
+
request.setHeader('baggage', (0, common_js_1.withTtS)(typeof prev === 'string' ? prev : Array.isArray(prev) ? prev.join(',') : undefined, decision.sampled));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// headers already flushed: leave the request untouched
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!decision.sampled) {
|
|
115
|
+
host.noteOverhead((0, common_js_1.hrNow)() - t);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
host.noteSampled();
|
|
119
|
+
const limit = san.policy.captureLimit; // max(max_body_bytes, bodies.max_large_body_bytes) + 1 (sanitizer.md 9.0, 22.5)
|
|
120
|
+
const st = {
|
|
121
|
+
san,
|
|
122
|
+
decision,
|
|
123
|
+
urlBase,
|
|
124
|
+
traceId: tr.traceId,
|
|
125
|
+
spanId: (0, context_js_1.newSpanId)(),
|
|
126
|
+
parentSpanId: tr.parentSpanId,
|
|
127
|
+
t0: performance.now(),
|
|
128
|
+
tsMs: Date.now(),
|
|
129
|
+
reqTap: new body_js_1.BodyTap(limit),
|
|
130
|
+
resTap: new body_js_1.BodyTap(limit),
|
|
131
|
+
done: false,
|
|
132
|
+
late: typeof request._header === 'string' || request.writableEnded || request.writableLength > 0,
|
|
133
|
+
overheadNs: 0,
|
|
134
|
+
};
|
|
135
|
+
(0, inbound_js_1.tapWrites)(request, st.reqTap);
|
|
136
|
+
request[kState] = st;
|
|
137
|
+
// 'close' never changes the app's error handling (unlike an 'error' listener).
|
|
138
|
+
request.once('close', () => {
|
|
139
|
+
if (!st.response)
|
|
140
|
+
complete(host, request, false);
|
|
141
|
+
});
|
|
142
|
+
st.overheadNs = (0, common_js_1.hrNow)() - t;
|
|
143
|
+
}
|
|
144
|
+
function complete(host, request, ended) {
|
|
145
|
+
const st = request[kState];
|
|
146
|
+
if (!st || st.done)
|
|
147
|
+
return;
|
|
148
|
+
const t = (0, common_js_1.hrNow)();
|
|
149
|
+
st.done = true;
|
|
150
|
+
request[kState] = undefined;
|
|
151
|
+
if (st.timer)
|
|
152
|
+
clearTimeout(st.timer);
|
|
153
|
+
try {
|
|
154
|
+
const latencyMs = Math.max(0, Math.floor(performance.now() - st.t0));
|
|
155
|
+
const res = st.response;
|
|
156
|
+
const netError = res ? undefined : st.netError ?? 'other';
|
|
157
|
+
const status = res?.statusCode;
|
|
158
|
+
const resConsumed = ended && st.resTap.ended;
|
|
159
|
+
const reqBody = st.reqTap.bytes();
|
|
160
|
+
const resBody = res && resConsumed ? st.resTap.bytes() : EMPTY;
|
|
161
|
+
const wire = {
|
|
162
|
+
dir: 'outbound',
|
|
163
|
+
url_base: st.urlBase,
|
|
164
|
+
method: request.method ?? 'GET',
|
|
165
|
+
target: request.path ?? '/',
|
|
166
|
+
inherited: st.decision.by === 'inherited' ? st.decision.sampled : undefined,
|
|
167
|
+
req_headers: (0, common_js_1.outgoingHeadersWire)(request),
|
|
168
|
+
req_body: reqBody,
|
|
169
|
+
req_body_total: st.late ? 'unknown' : st.reqTap.total,
|
|
170
|
+
};
|
|
171
|
+
if (netError) {
|
|
172
|
+
wire.net_error = netError;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
wire.status = status;
|
|
176
|
+
wire.resp_headers = res.rawHeaders;
|
|
177
|
+
wire.resp_body = resBody;
|
|
178
|
+
wire.resp_body_total = st.resTap.total;
|
|
179
|
+
wire.resp_body_consumed = resConsumed;
|
|
180
|
+
}
|
|
181
|
+
host.submit({
|
|
182
|
+
sanitizer: st.san,
|
|
183
|
+
decision: st.decision,
|
|
184
|
+
dir: 'outbound',
|
|
185
|
+
tsMs: st.tsMs,
|
|
186
|
+
latencyMs,
|
|
187
|
+
traceId: st.traceId,
|
|
188
|
+
spanId: st.spanId,
|
|
189
|
+
parentSpanId: st.parentSpanId,
|
|
190
|
+
status,
|
|
191
|
+
netError,
|
|
192
|
+
heldBytes: reqBody.length + resBody.length,
|
|
193
|
+
wire,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
host.noteError('capture_outbound_complete');
|
|
198
|
+
}
|
|
199
|
+
host.noteOverhead(st.overheadNs + ((0, common_js_1.hrNow)() - t));
|
|
200
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Outbound capture for fetch / undici through its diagnostics channels
|
|
3
|
+
// (`undici:request:create`, `:headers`, `:bodyChunkSent`, `:bodyChunkReceived`,
|
|
4
|
+
// `:trailers`, `:error`). Body chunk channels exist in recent undici releases (Node 24);
|
|
5
|
+
// where they are missing, bodies the SDK cannot see are reported as unknown length
|
|
6
|
+
// (sanitizer.md 9.0: `trunc:size`), never guessed.
|
|
7
|
+
//
|
|
8
|
+
// Response chunks are observed before fetch removes the content-encoding, so the body is
|
|
9
|
+
// passed as encoded bytes (`*_body_decoded = false`) and decoded by the sanitizer.
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.installOutboundUndici = installOutboundUndici;
|
|
15
|
+
const node_diagnostics_channel_1 = __importDefault(require("node:diagnostics_channel"));
|
|
16
|
+
const body_js_1 = require("./body.js");
|
|
17
|
+
const common_js_1 = require("./common.js");
|
|
18
|
+
const context_js_1 = require("./context.js");
|
|
19
|
+
const outbound_http_js_1 = require("./outbound-http.js");
|
|
20
|
+
const EMPTY = Buffer.alloc(0);
|
|
21
|
+
const states = new WeakMap();
|
|
22
|
+
/** undici headers (flat array of Buffer/string, or a header block string) as flat latin1 [name, value, ...] strings. */
|
|
23
|
+
function headerFlat(h) {
|
|
24
|
+
const out = [];
|
|
25
|
+
if (Array.isArray(h)) {
|
|
26
|
+
for (let i = 0; i + 1 < h.length; i += 2) {
|
|
27
|
+
const k = h[i];
|
|
28
|
+
const v = h[i + 1];
|
|
29
|
+
const name = Buffer.isBuffer(k) ? k.toString('latin1') : String(k);
|
|
30
|
+
if (Array.isArray(v))
|
|
31
|
+
for (const one of v)
|
|
32
|
+
out.push(name, Buffer.isBuffer(one) ? one.toString('latin1') : String(one));
|
|
33
|
+
else
|
|
34
|
+
out.push(name, Buffer.isBuffer(v) ? v.toString('latin1') : String(v));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (typeof h === 'string') {
|
|
38
|
+
for (const line of h.split('\r\n')) {
|
|
39
|
+
const c = line.indexOf(':');
|
|
40
|
+
if (c > 0)
|
|
41
|
+
out.push(line.slice(0, c), line.slice(c + 1));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function hasHeader(h, lower) {
|
|
47
|
+
const f = headerFlat(h);
|
|
48
|
+
for (let i = 0; i + 1 < f.length; i += 2)
|
|
49
|
+
if (f[i].toLowerCase() === lower)
|
|
50
|
+
return f[i + 1];
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
function installOutboundUndici(host) {
|
|
54
|
+
const subs = [];
|
|
55
|
+
const on = (name, fn) => {
|
|
56
|
+
const h = (m) => {
|
|
57
|
+
try {
|
|
58
|
+
fn(m);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
host.noteError('capture_undici');
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
node_diagnostics_channel_1.default.subscribe(name, h);
|
|
65
|
+
subs.push([name, h]);
|
|
66
|
+
};
|
|
67
|
+
// Whether this undici publishes body chunks is learnt from the first chunk seen.
|
|
68
|
+
const seen = { sent: false, received: false };
|
|
69
|
+
on('undici:request:create', ({ request }) => {
|
|
70
|
+
if (common_js_1.internal.depth > 0)
|
|
71
|
+
return;
|
|
72
|
+
const t = (0, common_js_1.hrNow)();
|
|
73
|
+
const san = host.active();
|
|
74
|
+
if (!san)
|
|
75
|
+
return;
|
|
76
|
+
host.noteSeen();
|
|
77
|
+
const ctx = context_js_1.als.getStore();
|
|
78
|
+
if (ctx !== undefined && !ctx.sampled && !host.propagates()) {
|
|
79
|
+
host.noteOverhead((0, common_js_1.hrNow)() - t);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const origin = String(request.origin ?? '');
|
|
83
|
+
const tr = (0, common_js_1.outboundTrace)(ctx);
|
|
84
|
+
const decision = san.decide({ dir: 'outbound', req_headers: [], trace_id: tr.samplingTraceId, inherited: ctx?.sampled });
|
|
85
|
+
if (host.mayPropagate(origin) && typeof request.addHeader === 'function') {
|
|
86
|
+
const prev = hasHeader(request.headers, 'baggage');
|
|
87
|
+
if (prev === undefined)
|
|
88
|
+
request.addHeader('baggage', (0, common_js_1.withTtS)(undefined, decision.sampled));
|
|
89
|
+
else
|
|
90
|
+
request.addHeader('baggage', 'tt.s=' + (decision.sampled ? '1' : '0'));
|
|
91
|
+
}
|
|
92
|
+
if (!decision.sampled) {
|
|
93
|
+
host.noteOverhead((0, common_js_1.hrNow)() - t);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
host.noteSampled();
|
|
97
|
+
const limit = san.policy.captureLimit; // max(max_body_bytes, bodies.max_large_body_bytes) + 1 (sanitizer.md 9.0, 22.5)
|
|
98
|
+
const st = {
|
|
99
|
+
san,
|
|
100
|
+
decision,
|
|
101
|
+
urlBase: origin,
|
|
102
|
+
method: String(request.method ?? 'GET'),
|
|
103
|
+
path: String(request.path ?? '/'),
|
|
104
|
+
traceId: tr.traceId,
|
|
105
|
+
spanId: (0, context_js_1.newSpanId)(),
|
|
106
|
+
parentSpanId: tr.parentSpanId,
|
|
107
|
+
t0: performance.now(),
|
|
108
|
+
tsMs: Date.now(),
|
|
109
|
+
reqTap: new body_js_1.BodyTap(limit),
|
|
110
|
+
reqBodyKnown: false,
|
|
111
|
+
resTap: new body_js_1.BodyTap(limit),
|
|
112
|
+
done: false,
|
|
113
|
+
overheadNs: 0,
|
|
114
|
+
};
|
|
115
|
+
const b = request.body;
|
|
116
|
+
if (b === null || b === undefined) {
|
|
117
|
+
st.reqBodyKnown = true;
|
|
118
|
+
}
|
|
119
|
+
else if (typeof b === 'string' || b instanceof Uint8Array) {
|
|
120
|
+
st.reqTap.add(b);
|
|
121
|
+
st.reqBodyKnown = true;
|
|
122
|
+
}
|
|
123
|
+
// Streamed bodies are counted by bodyChunkSent, when this undici publishes it.
|
|
124
|
+
states.set(request, st);
|
|
125
|
+
st.overheadNs = (0, common_js_1.hrNow)() - t;
|
|
126
|
+
});
|
|
127
|
+
on('undici:request:bodyChunkSent', ({ request, chunk }) => {
|
|
128
|
+
seen.sent = true;
|
|
129
|
+
const st = states.get(request);
|
|
130
|
+
if (!st)
|
|
131
|
+
return;
|
|
132
|
+
const b = request.body;
|
|
133
|
+
if (typeof b === 'string' || b instanceof Uint8Array)
|
|
134
|
+
return; // already taken from the request object
|
|
135
|
+
st.reqTap.add(chunk);
|
|
136
|
+
});
|
|
137
|
+
on('undici:request:headers', ({ request, response }) => {
|
|
138
|
+
const st = states.get(request);
|
|
139
|
+
if (!st)
|
|
140
|
+
return;
|
|
141
|
+
const r = response;
|
|
142
|
+
st.status = typeof r.statusCode === 'number' ? r.statusCode : undefined;
|
|
143
|
+
st.respHeaders = headerFlat(r.headers);
|
|
144
|
+
st.reqHeaders = headerFlat(request.headers);
|
|
145
|
+
st.timer = setTimeout(() => complete(host, request, false), outbound_http_js_1.UNREAD_BODY_TIMEOUT_MS);
|
|
146
|
+
st.timer.unref();
|
|
147
|
+
});
|
|
148
|
+
on('undici:request:bodyChunkReceived', ({ request, chunk }) => {
|
|
149
|
+
seen.received = true;
|
|
150
|
+
const st = states.get(request);
|
|
151
|
+
if (st)
|
|
152
|
+
st.resTap.add(chunk);
|
|
153
|
+
});
|
|
154
|
+
on('undici:request:trailers', ({ request }) => {
|
|
155
|
+
const st = states.get(request);
|
|
156
|
+
if (!st)
|
|
157
|
+
return;
|
|
158
|
+
st.resTap.ended = true;
|
|
159
|
+
complete(host, request, true);
|
|
160
|
+
});
|
|
161
|
+
on('undici:request:error', ({ request, error }) => {
|
|
162
|
+
const st = states.get(request);
|
|
163
|
+
if (!st)
|
|
164
|
+
return;
|
|
165
|
+
if (st.status === undefined)
|
|
166
|
+
st.netError = (0, common_js_1.netErrorOf)(error);
|
|
167
|
+
complete(host, request, false);
|
|
168
|
+
});
|
|
169
|
+
// Without the received-chunk channel the response body is never visible: a response is
|
|
170
|
+
// complete only if chunks were seen, the channel is known to work, or it declares no body.
|
|
171
|
+
const respBodyVisible = (st) => {
|
|
172
|
+
if (st.resTap.total > 0 || seen.received)
|
|
173
|
+
return true;
|
|
174
|
+
const rh = st.respHeaders ?? [];
|
|
175
|
+
for (let i = 0; i + 1 < rh.length; i += 2)
|
|
176
|
+
if (rh[i].toLowerCase() === 'content-length')
|
|
177
|
+
return rh[i + 1].trim() === '0';
|
|
178
|
+
return false;
|
|
179
|
+
};
|
|
180
|
+
function complete(h, request, ended) {
|
|
181
|
+
const st = states.get(request);
|
|
182
|
+
if (!st || st.done)
|
|
183
|
+
return;
|
|
184
|
+
const t = (0, common_js_1.hrNow)();
|
|
185
|
+
st.done = true;
|
|
186
|
+
states.delete(request);
|
|
187
|
+
if (st.timer)
|
|
188
|
+
clearTimeout(st.timer);
|
|
189
|
+
try {
|
|
190
|
+
const latencyMs = Math.max(0, Math.floor(performance.now() - st.t0));
|
|
191
|
+
const netError = st.status === undefined ? st.netError ?? 'other' : undefined;
|
|
192
|
+
const consumed = ended && respBodyVisible(st);
|
|
193
|
+
if (!st.reqBodyKnown && (st.reqTap.total > 0 || seen.sent))
|
|
194
|
+
st.reqBodyKnown = true;
|
|
195
|
+
const reqHeaders = st.reqHeaders ?? headerFlat(request.headers);
|
|
196
|
+
const reqBody = st.reqTap.bytes();
|
|
197
|
+
const resBody = consumed ? st.resTap.bytes() : EMPTY;
|
|
198
|
+
const wire = {
|
|
199
|
+
dir: 'outbound',
|
|
200
|
+
url_base: st.urlBase,
|
|
201
|
+
method: st.method,
|
|
202
|
+
target: st.path,
|
|
203
|
+
inherited: st.decision.by === 'inherited' ? st.decision.sampled : undefined,
|
|
204
|
+
req_headers: reqHeaders,
|
|
205
|
+
req_body: reqBody,
|
|
206
|
+
req_body_total: st.reqBodyKnown ? st.reqTap.total : 'unknown',
|
|
207
|
+
};
|
|
208
|
+
if (netError) {
|
|
209
|
+
wire.net_error = netError;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
wire.status = st.status;
|
|
213
|
+
wire.resp_headers = st.respHeaders ?? [];
|
|
214
|
+
wire.resp_body = resBody;
|
|
215
|
+
wire.resp_body_total = st.resTap.total;
|
|
216
|
+
wire.resp_body_consumed = consumed;
|
|
217
|
+
}
|
|
218
|
+
h.submit({
|
|
219
|
+
sanitizer: st.san,
|
|
220
|
+
decision: st.decision,
|
|
221
|
+
dir: 'outbound',
|
|
222
|
+
tsMs: st.tsMs,
|
|
223
|
+
latencyMs,
|
|
224
|
+
traceId: st.traceId,
|
|
225
|
+
spanId: st.spanId,
|
|
226
|
+
parentSpanId: st.parentSpanId,
|
|
227
|
+
status: st.status,
|
|
228
|
+
netError,
|
|
229
|
+
heldBytes: reqBody.length + resBody.length,
|
|
230
|
+
wire,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
h.noteError('capture_undici_complete');
|
|
235
|
+
}
|
|
236
|
+
h.noteOverhead(st.overheadNs + ((0, common_js_1.hrNow)() - t));
|
|
237
|
+
}
|
|
238
|
+
return () => {
|
|
239
|
+
for (const [name, fn] of subs)
|
|
240
|
+
node_diagnostics_channel_1.default.unsubscribe(name, fn);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Exchange, NetError } from '../sanitizer/sanitizer.js';
|
|
2
|
+
/** Flat [name, value, name, value ...] latin1 strings, or a serialized HTTP header block. */
|
|
3
|
+
export type WireHeaders = string[] | string;
|
|
4
|
+
export interface WireExchange {
|
|
5
|
+
dir: 'inbound' | 'outbound';
|
|
6
|
+
service_name?: string;
|
|
7
|
+
url_base?: string;
|
|
8
|
+
method: string;
|
|
9
|
+
/** Request target as received/sent (path and optional query). */
|
|
10
|
+
target: string;
|
|
11
|
+
framework?: string;
|
|
12
|
+
framework_route?: string;
|
|
13
|
+
trace_id?: string;
|
|
14
|
+
inherited?: boolean;
|
|
15
|
+
req_headers: WireHeaders;
|
|
16
|
+
req_body: Uint8Array;
|
|
17
|
+
req_body_total?: number | 'unknown';
|
|
18
|
+
req_body_consumed?: boolean;
|
|
19
|
+
status?: number;
|
|
20
|
+
net_error?: NetError;
|
|
21
|
+
resp_headers?: WireHeaders;
|
|
22
|
+
resp_body?: Uint8Array;
|
|
23
|
+
resp_body_total?: number | 'unknown';
|
|
24
|
+
resp_body_consumed?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Wire exchange to the sanitizer's input (sanitizer.md 1). */
|
|
27
|
+
export declare function wireToExchange(w: WireExchange): Exchange;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Wire form of a captured exchange: plain strings, numbers and byte arrays only, so it can
|
|
3
|
+
// be posted to a sanitizer worker thread (structured clone) or converted in place.
|
|
4
|
+
//
|
|
5
|
+
// The capture hooks fill this in with the least possible work (they keep references to the
|
|
6
|
+
// strings Node already built); the conversion to the sanitizer's `Exchange` (header
|
|
7
|
+
// buffers, request-target split, header-block parsing) happens later, off the request
|
|
8
|
+
// path, and on the worker thread when one is used.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.wireToExchange = wireToExchange;
|
|
11
|
+
const common_js_1 = require("./common.js");
|
|
12
|
+
function pairs(h) {
|
|
13
|
+
if (h === undefined)
|
|
14
|
+
return [];
|
|
15
|
+
if (typeof h === 'string')
|
|
16
|
+
return (0, common_js_1.parseHeaderBlock)(h);
|
|
17
|
+
const out = [];
|
|
18
|
+
for (let i = 0; i + 1 < h.length; i += 2)
|
|
19
|
+
out.push([h[i], Buffer.from(h[i + 1], 'latin1')]);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
/** Zero-copy view of body bytes that arrived through a message port or the tap. */
|
|
23
|
+
function asBuffer(b) {
|
|
24
|
+
if (b === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
return Buffer.isBuffer(b) ? b : Buffer.from(b.buffer, b.byteOffset, b.byteLength);
|
|
27
|
+
}
|
|
28
|
+
/** Wire exchange to the sanitizer's input (sanitizer.md 1). */
|
|
29
|
+
function wireToExchange(w) {
|
|
30
|
+
const { path, query } = (0, common_js_1.splitTarget)(w.target);
|
|
31
|
+
const ex = {
|
|
32
|
+
dir: w.dir,
|
|
33
|
+
method: w.method,
|
|
34
|
+
raw_path: path,
|
|
35
|
+
raw_query: query,
|
|
36
|
+
req_headers: pairs(w.req_headers),
|
|
37
|
+
req_body: asBuffer(w.req_body),
|
|
38
|
+
};
|
|
39
|
+
if (w.service_name !== undefined)
|
|
40
|
+
ex.service_name = w.service_name;
|
|
41
|
+
if (w.url_base !== undefined)
|
|
42
|
+
ex.url_base = w.url_base;
|
|
43
|
+
if (w.framework_route !== undefined) {
|
|
44
|
+
ex.framework = w.framework ?? 'express';
|
|
45
|
+
ex.framework_route = w.framework_route;
|
|
46
|
+
}
|
|
47
|
+
if (w.trace_id !== undefined)
|
|
48
|
+
ex.trace_id = w.trace_id;
|
|
49
|
+
if (w.inherited !== undefined)
|
|
50
|
+
ex.inherited = w.inherited;
|
|
51
|
+
if (w.req_body_total !== undefined)
|
|
52
|
+
ex.req_body_total = w.req_body_total;
|
|
53
|
+
if (w.req_body_consumed !== undefined)
|
|
54
|
+
ex.req_body_consumed = w.req_body_consumed;
|
|
55
|
+
if (w.net_error) {
|
|
56
|
+
ex.net_error = w.net_error;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
ex.status = w.status;
|
|
60
|
+
ex.resp_headers = pairs(w.resp_headers);
|
|
61
|
+
ex.resp_body = asBuffer(w.resp_body);
|
|
62
|
+
if (w.resp_body_total !== undefined)
|
|
63
|
+
ex.resp_body_total = w.resp_body_total;
|
|
64
|
+
if (w.resp_body_consumed !== undefined)
|
|
65
|
+
ex.resp_body_consumed = w.resp_body_consumed;
|
|
66
|
+
}
|
|
67
|
+
return ex;
|
|
68
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type OMap } from '../sanitizer/json.js';
|
|
2
|
+
import { type NetError } from '../sanitizer/sanitizer.js';
|
|
3
|
+
export interface EventMeta {
|
|
4
|
+
policyId: string;
|
|
5
|
+
policyVersion: number;
|
|
6
|
+
policyHash: string;
|
|
7
|
+
sourceId: string;
|
|
8
|
+
dir: 'inbound' | 'outbound';
|
|
9
|
+
tsMs: number;
|
|
10
|
+
latencyMs: number;
|
|
11
|
+
status?: number;
|
|
12
|
+
netError?: NetError;
|
|
13
|
+
traceId?: string;
|
|
14
|
+
spanId?: string;
|
|
15
|
+
parentSpanId?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Full canonical event from the sanitizer output (a map or its serialized top-level members)
|
|
19
|
+
* and the fields the source fills. Null if it exceeds 512 KiB (counted as sanitizer error).
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildEvent(out: OMap | Map<string, string>, m: EventMeta): string | null;
|
|
22
|
+
/** Marks a map of already serialized members (Result.parts) for buildEvent. */
|
|
23
|
+
export declare function serializedParts(parts: Map<string, string>): Map<string, string>;
|
|
24
|
+
export interface QueuedEvent {
|
|
25
|
+
/** Canonical event JSON. Empty when `record` already carries it (worker pipeline). */
|
|
26
|
+
json: string;
|
|
27
|
+
tsMs: number;
|
|
28
|
+
traceId?: string;
|
|
29
|
+
spanId?: string;
|
|
30
|
+
policyHash: string;
|
|
31
|
+
/** Ready-made OTLP logRecord (see makeRecord): the batch body is then only joined. */
|
|
32
|
+
record?: string;
|
|
33
|
+
/** Bytes charged to the export queue; defaults to json.length. */
|
|
34
|
+
size?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface Resource {
|
|
37
|
+
serviceName: string;
|
|
38
|
+
sourceId: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* OTLP/HTTP JSON logRecord for one event (event.md 4.2), written by hand: the canonical
|
|
42
|
+
* event is escaped once, here, and the batch body is a join of ready-made records.
|
|
43
|
+
*/
|
|
44
|
+
export declare function makeRecord(e: QueuedEvent, observedMs: number): string;
|
|
45
|
+
/** OTLP/HTTP JSON body for a batch of events. */
|
|
46
|
+
export declare function otlpBody(events: QueuedEvent[], res: Resource, nowMs: number): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Event assembly (event.md 1) and the OTLP/HTTP JSON logs body (event.md 4.2), written by hand.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.buildEvent = buildEvent;
|
|
5
|
+
exports.serializedParts = serializedParts;
|
|
6
|
+
exports.makeRecord = makeRecord;
|
|
7
|
+
exports.otlpBody = otlpBody;
|
|
8
|
+
const json_js_1 = require("../sanitizer/json.js");
|
|
9
|
+
const sanitizer_js_1 = require("../sanitizer/sanitizer.js");
|
|
10
|
+
const version_js_1 = require("../version.js");
|
|
11
|
+
const HEX32 = /^[0-9a-f]{32}$/;
|
|
12
|
+
const HEX16 = /^[0-9a-f]{16}$/;
|
|
13
|
+
/**
|
|
14
|
+
* Full canonical event from the sanitizer output (a map or its serialized top-level members)
|
|
15
|
+
* and the fields the source fills. Null if it exceeds 512 KiB (counted as sanitizer error).
|
|
16
|
+
*/
|
|
17
|
+
function buildEvent(out, m) {
|
|
18
|
+
const parts = new Map();
|
|
19
|
+
const pre = isSerialized(out);
|
|
20
|
+
for (const [k, v] of out)
|
|
21
|
+
parts.set(k, pre ? v : (0, json_js_1.writeCanonical)(v));
|
|
22
|
+
const set = (k, v) => parts.set(k, (0, json_js_1.writeCanonical)(v));
|
|
23
|
+
set('schema', 'tt.http.exchange/v1');
|
|
24
|
+
set('policy', new Map([['id', m.policyId], ['version', new json_js_1.CNum(String(m.policyVersion))], ['hash', m.policyHash]]));
|
|
25
|
+
set('source', new Map([['kind', version_js_1.SOURCE_KIND], ['version', version_js_1.SDK_VERSION], ['id', m.sourceId]]));
|
|
26
|
+
set('ts', new Date(m.tsMs).toISOString());
|
|
27
|
+
set('latency_ms', new json_js_1.CNum(String(Math.max(0, Math.floor(m.latencyMs)))));
|
|
28
|
+
set('dir', m.dir);
|
|
29
|
+
if (m.netError) {
|
|
30
|
+
set('net_error', m.netError);
|
|
31
|
+
parts.delete('resp');
|
|
32
|
+
}
|
|
33
|
+
else if (typeof m.status === 'number' && m.status >= 100 && m.status <= 599) {
|
|
34
|
+
set('status', new json_js_1.CNum(String(m.status)));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (m.traceId && HEX32.test(m.traceId))
|
|
40
|
+
set('trace_id', m.traceId);
|
|
41
|
+
if (m.spanId && HEX16.test(m.spanId))
|
|
42
|
+
set('span_id', m.spanId);
|
|
43
|
+
if (m.parentSpanId && HEX16.test(m.parentSpanId))
|
|
44
|
+
set('parent_span_id', m.parentSpanId);
|
|
45
|
+
const s = (0, sanitizer_js_1.composeCanonical)(parts);
|
|
46
|
+
return s.length > sanitizer_js_1.MAX_EVENT_BYTES ? null : s;
|
|
47
|
+
}
|
|
48
|
+
const serialized = new WeakSet();
|
|
49
|
+
/** Marks a map of already serialized members (Result.parts) for buildEvent. */
|
|
50
|
+
function serializedParts(parts) {
|
|
51
|
+
serialized.add(parts);
|
|
52
|
+
return parts;
|
|
53
|
+
}
|
|
54
|
+
function isSerialized(m) {
|
|
55
|
+
return serialized.has(m);
|
|
56
|
+
}
|
|
57
|
+
const attr = (key, value) => ({ key, value: { stringValue: value } });
|
|
58
|
+
const nanos = (ms) => (ms > 0 ? String(Math.trunc(ms)) + '000000' : '0');
|
|
59
|
+
/**
|
|
60
|
+
* OTLP/HTTP JSON logRecord for one event (event.md 4.2), written by hand: the canonical
|
|
61
|
+
* event is escaped once, here, and the batch body is a join of ready-made records.
|
|
62
|
+
*/
|
|
63
|
+
function makeRecord(e, observedMs) {
|
|
64
|
+
let r = '{"timeUnixNano":"' + nanos(e.tsMs) + '","observedTimeUnixNano":"' + nanos(observedMs)
|
|
65
|
+
+ '","severityNumber":9,"severityText":"INFO","eventName":"tt.http.exchange"';
|
|
66
|
+
if (e.traceId && HEX32.test(e.traceId))
|
|
67
|
+
r += ',"traceId":"' + e.traceId + '"';
|
|
68
|
+
if (e.spanId && HEX16.test(e.spanId))
|
|
69
|
+
r += ',"spanId":"' + e.spanId + '"';
|
|
70
|
+
r += ',"attributes":[{"key":"tt.schema","value":{"stringValue":"tt.http.exchange/v1"}},{"key":"tt.policy.hash","value":{"stringValue":'
|
|
71
|
+
+ JSON.stringify(e.policyHash) + '}}],"body":{"stringValue":' + JSON.stringify(e.json) + '}}';
|
|
72
|
+
return r;
|
|
73
|
+
}
|
|
74
|
+
/** OTLP/HTTP JSON body for a batch of events. */
|
|
75
|
+
function otlpBody(events, res, nowMs) {
|
|
76
|
+
let recs = '';
|
|
77
|
+
for (let i = 0; i < events.length; i++) {
|
|
78
|
+
const e = events[i];
|
|
79
|
+
if (i > 0)
|
|
80
|
+
recs += ',';
|
|
81
|
+
recs += e.record ?? makeRecord(e, nowMs);
|
|
82
|
+
}
|
|
83
|
+
return '{"resourceLogs":[{"resource":' + JSON.stringify({
|
|
84
|
+
attributes: [
|
|
85
|
+
attr('service.name', res.serviceName),
|
|
86
|
+
attr('tt.source.kind', version_js_1.SOURCE_KIND),
|
|
87
|
+
attr('tt.source.version', version_js_1.SDK_VERSION),
|
|
88
|
+
attr('tt.source.id', res.sourceId),
|
|
89
|
+
],
|
|
90
|
+
}) + ',"scopeLogs":[{"scope":' + JSON.stringify({ name: 'traffic-twin', version: version_js_1.SDK_VERSION }) + ',"logRecords":[' + recs + ']}]}]}';
|
|
91
|
+
}
|