@rootcat/node 0.0.0-stage → 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/CHANGELOG.md +83 -0
- package/LICENSE +202 -0
- package/README.md +294 -2
- package/dist/attach.d.ts +1 -0
- package/dist/attach.js +24 -0
- package/dist/attach.js.map +1 -0
- package/dist/callsite.d.ts +109 -0
- package/dist/callsite.js +565 -0
- package/dist/callsite.js.map +1 -0
- package/dist/coexist.d.ts +183 -0
- package/dist/coexist.js +587 -0
- package/dist/coexist.js.map +1 -0
- package/dist/config.d.ts +75 -0
- package/dist/config.js +182 -0
- package/dist/config.js.map +1 -0
- package/dist/drizzle.d.ts +21 -0
- package/dist/drizzle.js +127 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/esm.mjs +57 -0
- package/dist/esmhook.d.ts +52 -0
- package/dist/esmhook.js +158 -0
- package/dist/esmhook.js.map +1 -0
- package/dist/iitm-hook.mjs +83 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/intake.d.ts +40 -0
- package/dist/intake.js +164 -0
- package/dist/intake.js.map +1 -0
- package/dist/keys.d.ts +20 -0
- package/dist/keys.js +33 -0
- package/dist/keys.js.map +1 -0
- package/dist/notices.d.ts +2 -0
- package/dist/notices.js +21 -0
- package/dist/notices.js.map +1 -0
- package/dist/otlp.d.ts +75 -0
- package/dist/otlp.js +449 -0
- package/dist/otlp.js.map +1 -0
- package/dist/pg.d.ts +16 -0
- package/dist/pg.js +170 -0
- package/dist/pg.js.map +1 -0
- package/dist/pgpool.d.ts +20 -0
- package/dist/pgpool.js +99 -0
- package/dist/pgpool.js.map +1 -0
- package/dist/postgresjs.d.ts +60 -0
- package/dist/postgresjs.js +400 -0
- package/dist/postgresjs.js.map +1 -0
- package/dist/privacy.d.ts +38 -0
- package/dist/privacy.js +161 -0
- package/dist/privacy.js.map +1 -0
- package/dist/processor.d.ts +197 -0
- package/dist/processor.js +494 -0
- package/dist/processor.js.map +1 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +12 -0
- package/dist/register.js.map +1 -0
- package/dist/sampler.d.ts +30 -0
- package/dist/sampler.js +101 -0
- package/dist/sampler.js.map +1 -0
- package/dist/sdk.d.ts +129 -0
- package/dist/sdk.js +606 -0
- package/dist/sdk.js.map +1 -0
- package/package.json +72 -4
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { type Attributes, type Context, type Span as ApiSpan, type SpanStatus } from '@opentelemetry/api';
|
|
2
|
+
import type { Resource } from '@opentelemetry/resources';
|
|
3
|
+
import type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
4
|
+
import { type CallSiteLocation } from './callsite.js';
|
|
5
|
+
import { type StatementMode } from './privacy.js';
|
|
6
|
+
export { callSiteContextKey } from './keys.js';
|
|
7
|
+
/**
|
|
8
|
+
* CallSiteSampling is the gate every capture goes through: the
|
|
9
|
+
* processor's own and the entry wrappers' (pg, pg pool, Drizzle,
|
|
10
|
+
* postgres.js), so a capture is never paid for a span that will not be
|
|
11
|
+
* exported or that the one-in-N setting skips.
|
|
12
|
+
*/
|
|
13
|
+
export declare class CallSiteSampling {
|
|
14
|
+
/** sample is the one-in-N setting; the benchmark changes it between blocks. */
|
|
15
|
+
sample: number;
|
|
16
|
+
/** ratio is the trace sampling share; the benchmark changes it between blocks. */
|
|
17
|
+
ratio: number;
|
|
18
|
+
private counter;
|
|
19
|
+
/** sample is ROOTCAT_CALLSITE_SAMPLE; ratio is ROOTCAT_SAMPLE. */
|
|
20
|
+
constructor(sample: number, ratio?: number);
|
|
21
|
+
/** take counts one occasion and reports whether it captures. */
|
|
22
|
+
take(): boolean;
|
|
23
|
+
/** on reports whether any capture happens at all. */
|
|
24
|
+
get on(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* carry is what an entry wrapper calls before the operation it wraps:
|
|
27
|
+
* it returns the context to run the operation in, or undefined to run
|
|
28
|
+
* it as it is. Under an unrecorded span nothing happens. Outside any
|
|
29
|
+
* span with a ratio below 1, the trace decision is made here (the
|
|
30
|
+
* sampler follows it), and an unsampled operation captures nothing. A
|
|
31
|
+
* site already carried by an outer layer of the same operation stands,
|
|
32
|
+
* unless override (a capture that costs nothing, such as postgres.js's
|
|
33
|
+
* own origin, and names the operation more exactly). Otherwise, one
|
|
34
|
+
* occasion in N, capture() runs and its site is carried.
|
|
35
|
+
*/
|
|
36
|
+
carry(capture: () => CallSiteLocation | undefined, override?: boolean): Context | undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* recorded reports whether a span in this process is part of a recorded
|
|
40
|
+
* trace: it is recording (beside an errors-only Sentry the package
|
|
41
|
+
* records spans without the sampled flag, see RecordOnlySampler), or it
|
|
42
|
+
* carries the flag (a span context an application wrapped itself, to
|
|
43
|
+
* continue a trace from a queued job).
|
|
44
|
+
*/
|
|
45
|
+
export declare function recorded(span: ApiSpan): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* droppedMark is the trace-state key on the spans the package itself
|
|
48
|
+
* decided not to record beside Sentry (see RecordOnlySampler), so they
|
|
49
|
+
* can be told from another SDK's unrecorded spans.
|
|
50
|
+
*/
|
|
51
|
+
export declare const droppedMark = "rootcat";
|
|
52
|
+
/**
|
|
53
|
+
* parentPolicy says how an unrecorded parent from another SDK counts.
|
|
54
|
+
* Beside an errors-only Sentry every span on Sentry's provider is
|
|
55
|
+
* unrecorded (Next.js starts its own through it), and the package's spans
|
|
56
|
+
* below one must not be lost: such a parent is passed through, as if the
|
|
57
|
+
* span had none, and the trace ratio decides on its trace id. Set once,
|
|
58
|
+
* when the package starts beside Sentry.
|
|
59
|
+
*/
|
|
60
|
+
export declare const parentPolicy: {
|
|
61
|
+
transparentForeign: boolean;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* localParent is the span in ctx when it was started in this process
|
|
65
|
+
* (not a parent from an incoming header), and counts as one (see
|
|
66
|
+
* parentPolicy).
|
|
67
|
+
*/
|
|
68
|
+
export declare function localParent(ctx: Context): ApiSpan | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* unsampled reports whether a span started under ctx would not be
|
|
71
|
+
* recorded (an unrecorded parent, or a root an entry wrapper decided
|
|
72
|
+
* against): the drivers then run untouched, without even a
|
|
73
|
+
* non-recording span or a context of their own.
|
|
74
|
+
*/
|
|
75
|
+
export declare function unsampled(ctx: Context): boolean;
|
|
76
|
+
/** runCarried runs fn under the context carry returned, if any. */
|
|
77
|
+
export declare function runCarried<T>(ctx: Context | undefined, fn: () => T): T;
|
|
78
|
+
/** CallSiteProcessorOptions configures the processor. */
|
|
79
|
+
export interface CallSiteProcessorOptions {
|
|
80
|
+
/** sampling gates the captures; a number is the one-in-N setting. */
|
|
81
|
+
sampling: CallSiteSampling | number;
|
|
82
|
+
/** depth bounds the frames walked. */
|
|
83
|
+
depth: number;
|
|
84
|
+
/** statement is passed to the privacy rules. */
|
|
85
|
+
statement: StatementMode;
|
|
86
|
+
/** isApp decides which files are application code (tests override). */
|
|
87
|
+
isApp?: (file: string) => boolean;
|
|
88
|
+
/**
|
|
89
|
+
* above is the function a capture from the processor starts below (the
|
|
90
|
+
* SDK tracer's startSpan), so the walk skips the SDK's own frames.
|
|
91
|
+
*/
|
|
92
|
+
above?: Function;
|
|
93
|
+
/**
|
|
94
|
+
* readOnly leaves the spans as they are: the processor sits in another
|
|
95
|
+
* SDK's provider, which sends those spans on. The call site and the
|
|
96
|
+
* privacy rules then apply only to the copy the package exports
|
|
97
|
+
* (exportView).
|
|
98
|
+
*/
|
|
99
|
+
readOnly?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* keepHostKeys leaves another SDK's keys (sentry.*) on the live span
|
|
102
|
+
* at its end: beside Sentry, Sentry reads an ended span still active in
|
|
103
|
+
* a request's context (a call made after the response), and its source
|
|
104
|
+
* must stay what UrlSource pinned. The exported copy drops them (see
|
|
105
|
+
* FlaggedExport).
|
|
106
|
+
*/
|
|
107
|
+
keepHostKeys?: boolean;
|
|
108
|
+
}
|
|
109
|
+
/** Attribute keys written. */
|
|
110
|
+
export declare const codeFilepath = "code.filepath";
|
|
111
|
+
export declare const codeLineno = "code.lineno";
|
|
112
|
+
export declare const codeFunction = "code.function";
|
|
113
|
+
/**
|
|
114
|
+
* eligible reports whether a span is a client operation worth a call
|
|
115
|
+
* site: client and producer spans, anything carrying db.system, and the
|
|
116
|
+
* operation spans of the ORMs that start their work in application code.
|
|
117
|
+
* A processor runs for recorded spans only, so every span here exports.
|
|
118
|
+
*/
|
|
119
|
+
export declare function eligible(span: ReadableSpan): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* clientOperationByOp reports whether a Sentry span is a client
|
|
122
|
+
* operation by its op, the one attribute Sentry sets when it starts a
|
|
123
|
+
* span (its postgres.js span gets db.system.name only after the start).
|
|
124
|
+
*/
|
|
125
|
+
export declare function clientOperationByOp(span: ReadableSpan): boolean;
|
|
126
|
+
/** CallSiteProcessor implements SpanProcessor. */
|
|
127
|
+
export declare class CallSiteProcessor implements SpanProcessor {
|
|
128
|
+
readonly sampling: CallSiteSampling;
|
|
129
|
+
private readonly depth;
|
|
130
|
+
private readonly statement;
|
|
131
|
+
private readonly isApp;
|
|
132
|
+
/** above is where a processor capture starts (the SDK tracer startSpan); set once the provider exists. */
|
|
133
|
+
above: Function | undefined;
|
|
134
|
+
readonly readOnly: boolean;
|
|
135
|
+
private readonly keepHostKeys;
|
|
136
|
+
private readonly sites;
|
|
137
|
+
/** captured counts spans given a call site (tests and the benchmark). */
|
|
138
|
+
captured: number;
|
|
139
|
+
constructor(options: CallSiteProcessorOptions);
|
|
140
|
+
/**
|
|
141
|
+
* assign gives a started span a call site found after its start (a
|
|
142
|
+
* driver's own origin, read once another SDK's span exists); it
|
|
143
|
+
* replaces any the processor found at the start.
|
|
144
|
+
*/
|
|
145
|
+
assign(span: object, site: CallSiteLocation): void;
|
|
146
|
+
/** siteOf is the call site the processor holds for a span. */
|
|
147
|
+
siteOf(span: object): CallSiteLocation | undefined;
|
|
148
|
+
onStart(span: Span, parentContext: Context): void;
|
|
149
|
+
/**
|
|
150
|
+
* locate takes a site already carried in the context or by the parent
|
|
151
|
+
* span (an entry wrapper paid for it, under the same gate), else, one
|
|
152
|
+
* occasion in N, captures the synchronous stack.
|
|
153
|
+
*/
|
|
154
|
+
private locate;
|
|
155
|
+
onEnd(span: ReadableSpan): void;
|
|
156
|
+
/**
|
|
157
|
+
* exportView is the span as the package exports it from a read-only
|
|
158
|
+
* processor: a copy of its attributes, events and status with the
|
|
159
|
+
* privacy rules and the call site applied, under the package's
|
|
160
|
+
* resource. The span itself is not touched.
|
|
161
|
+
*/
|
|
162
|
+
exportView(span: ReadableSpan, resource?: Resource): ReadableSpan;
|
|
163
|
+
/** statementName is a statement-shaped span name under the statement rule. */
|
|
164
|
+
private statementName;
|
|
165
|
+
shutdown(): Promise<void>;
|
|
166
|
+
forceFlush(): Promise<void>;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* routeFromHost drops the keys never exported and turns what a Sentry
|
|
170
|
+
* framework instrumentation wrote on the active span into the package's own
|
|
171
|
+
* vocabulary. @sentry/hono renames the server span to "GET /users/:id"
|
|
172
|
+
* once the route is known (sentry.source "route"): the template becomes
|
|
173
|
+
* http.route.
|
|
174
|
+
*/
|
|
175
|
+
export declare function routeFromHost(name: string, attributes: Record<string, unknown>): unknown;
|
|
176
|
+
/**
|
|
177
|
+
* exportedName is the name a span is exported under. Another SDK's HTTP
|
|
178
|
+
* span is named by the method and the path or URL it served or called
|
|
179
|
+
* (Sentry's: "GET /reset/3f9c...", "GET https://api.example.com/u/42"):
|
|
180
|
+
* the target is masked as an attribute URL is, unless the name is a
|
|
181
|
+
* route template a framework set (sentry.source "route").
|
|
182
|
+
*/
|
|
183
|
+
export declare function exportedName(name: string, source: unknown): string;
|
|
184
|
+
/** Overrides is what a view replaces; the rest reads through to the span. */
|
|
185
|
+
interface Overrides {
|
|
186
|
+
name?: string;
|
|
187
|
+
attributes?: Attributes;
|
|
188
|
+
events?: ReadableSpan['events'];
|
|
189
|
+
status?: SpanStatus;
|
|
190
|
+
resource?: Resource;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* view is a ReadableSpan with the span's fields except the given ones,
|
|
194
|
+
* and always flagged sampled: the package exports only spans it chose to
|
|
195
|
+
* record, whatever flag they carry inside the process.
|
|
196
|
+
*/
|
|
197
|
+
export declare function view(span: ReadableSpan, overrides?: Overrides): ReadableSpan;
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CallSiteProcessor = exports.codeFunction = exports.codeLineno = exports.codeFilepath = exports.parentPolicy = exports.droppedMark = exports.CallSiteSampling = exports.callSiteContextKey = void 0;
|
|
4
|
+
exports.recorded = recorded;
|
|
5
|
+
exports.localParent = localParent;
|
|
6
|
+
exports.unsampled = unsampled;
|
|
7
|
+
exports.runCarried = runCarried;
|
|
8
|
+
exports.eligible = eligible;
|
|
9
|
+
exports.clientOperationByOp = clientOperationByOp;
|
|
10
|
+
exports.routeFromHost = routeFromHost;
|
|
11
|
+
exports.exportedName = exportedName;
|
|
12
|
+
exports.view = view;
|
|
13
|
+
// CallSiteProcessor: a span processor that, when a client span starts,
|
|
14
|
+
// records the application frame that caused it as code.filepath,
|
|
15
|
+
// code.lineno and code.function, and applies the privacy rules to the
|
|
16
|
+
// span's attributes at start and at end.
|
|
17
|
+
//
|
|
18
|
+
// Where the frame comes from, in order: the synchronous stack at span
|
|
19
|
+
// start (the usual case: the driver was called from application code);
|
|
20
|
+
// else a call site an entry wrapper stored in the active context (a pg
|
|
21
|
+
// pool hands queries to a client on nextTick, where the stack is the
|
|
22
|
+
// pool's); else the parent span's call site (an ORM's operation span
|
|
23
|
+
// started in application code, its driver span on a later tick).
|
|
24
|
+
//
|
|
25
|
+
// In another SDK's provider (Sentry's, with Sentry tracing on) the
|
|
26
|
+
// processor is read-only: it finds the call site the same way but writes
|
|
27
|
+
// nothing on the span, which that SDK sends on; the package exports a
|
|
28
|
+
// copy with the site and the privacy rules applied (exportView).
|
|
29
|
+
const api_1 = require("@opentelemetry/api");
|
|
30
|
+
const callsite_js_1 = require("./callsite.js");
|
|
31
|
+
const keys_js_1 = require("./keys.js");
|
|
32
|
+
const privacy_js_1 = require("./privacy.js");
|
|
33
|
+
var keys_js_2 = require("./keys.js");
|
|
34
|
+
Object.defineProperty(exports, "callSiteContextKey", { enumerable: true, get: function () { return keys_js_2.callSiteContextKey; } });
|
|
35
|
+
/**
|
|
36
|
+
* CallSiteSampling is the gate every capture goes through: the
|
|
37
|
+
* processor's own and the entry wrappers' (pg, pg pool, Drizzle,
|
|
38
|
+
* postgres.js), so a capture is never paid for a span that will not be
|
|
39
|
+
* exported or that the one-in-N setting skips.
|
|
40
|
+
*/
|
|
41
|
+
class CallSiteSampling {
|
|
42
|
+
/** sample is the one-in-N setting; the benchmark changes it between blocks. */
|
|
43
|
+
sample;
|
|
44
|
+
/** ratio is the trace sampling share; the benchmark changes it between blocks. */
|
|
45
|
+
ratio;
|
|
46
|
+
counter = 0;
|
|
47
|
+
/** sample is ROOTCAT_CALLSITE_SAMPLE; ratio is ROOTCAT_SAMPLE. */
|
|
48
|
+
constructor(sample, ratio = 1) {
|
|
49
|
+
this.sample = Math.max(0, Math.floor(sample));
|
|
50
|
+
this.ratio = Math.min(1, Math.max(0, ratio));
|
|
51
|
+
}
|
|
52
|
+
/** take counts one occasion and reports whether it captures. */
|
|
53
|
+
take() {
|
|
54
|
+
if (this.sample === 0) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
this.counter++;
|
|
58
|
+
return this.counter % this.sample === 0;
|
|
59
|
+
}
|
|
60
|
+
/** on reports whether any capture happens at all. */
|
|
61
|
+
get on() {
|
|
62
|
+
return this.sample > 0;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* carry is what an entry wrapper calls before the operation it wraps:
|
|
66
|
+
* it returns the context to run the operation in, or undefined to run
|
|
67
|
+
* it as it is. Under an unrecorded span nothing happens. Outside any
|
|
68
|
+
* span with a ratio below 1, the trace decision is made here (the
|
|
69
|
+
* sampler follows it), and an unsampled operation captures nothing. A
|
|
70
|
+
* site already carried by an outer layer of the same operation stands,
|
|
71
|
+
* unless override (a capture that costs nothing, such as postgres.js's
|
|
72
|
+
* own origin, and names the operation more exactly). Otherwise, one
|
|
73
|
+
* occasion in N, capture() runs and its site is carried.
|
|
74
|
+
*/
|
|
75
|
+
carry(capture, override = false) {
|
|
76
|
+
const active = api_1.context.active();
|
|
77
|
+
const local = localParent(active);
|
|
78
|
+
let ctx = active;
|
|
79
|
+
// A remote parent (an incoming traceparent) decides nothing: see the sampler.
|
|
80
|
+
if (local) {
|
|
81
|
+
if (!recorded(local)) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (this.ratio < 1 && active.getValue(keys_js_1.rootDecisionKey) === undefined) {
|
|
86
|
+
const decided = Math.random() < this.ratio;
|
|
87
|
+
ctx = ctx.setValue(keys_js_1.rootDecisionKey, decided);
|
|
88
|
+
if (!decided) {
|
|
89
|
+
return ctx;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (active.getValue(keys_js_1.rootDecisionKey) === false) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const existing = active.getValue(keys_js_1.callSiteContextKey) !== undefined;
|
|
96
|
+
if ((existing && !override) || (!existing && !this.take())) {
|
|
97
|
+
return ctx === active ? undefined : ctx;
|
|
98
|
+
}
|
|
99
|
+
const site = capture();
|
|
100
|
+
if (site) {
|
|
101
|
+
ctx = ctx.setValue(keys_js_1.callSiteContextKey, site);
|
|
102
|
+
}
|
|
103
|
+
return ctx === active ? undefined : ctx;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.CallSiteSampling = CallSiteSampling;
|
|
107
|
+
/**
|
|
108
|
+
* recorded reports whether a span in this process is part of a recorded
|
|
109
|
+
* trace: it is recording (beside an errors-only Sentry the package
|
|
110
|
+
* records spans without the sampled flag, see RecordOnlySampler), or it
|
|
111
|
+
* carries the flag (a span context an application wrapped itself, to
|
|
112
|
+
* continue a trace from a queued job).
|
|
113
|
+
*/
|
|
114
|
+
function recorded(span) {
|
|
115
|
+
return span.isRecording() || (span.spanContext().traceFlags & api_1.TraceFlags.SAMPLED) !== 0;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* droppedMark is the trace-state key on the spans the package itself
|
|
119
|
+
* decided not to record beside Sentry (see RecordOnlySampler), so they
|
|
120
|
+
* can be told from another SDK's unrecorded spans.
|
|
121
|
+
*/
|
|
122
|
+
exports.droppedMark = 'rootcat';
|
|
123
|
+
/**
|
|
124
|
+
* parentPolicy says how an unrecorded parent from another SDK counts.
|
|
125
|
+
* Beside an errors-only Sentry every span on Sentry's provider is
|
|
126
|
+
* unrecorded (Next.js starts its own through it), and the package's spans
|
|
127
|
+
* below one must not be lost: such a parent is passed through, as if the
|
|
128
|
+
* span had none, and the trace ratio decides on its trace id. Set once,
|
|
129
|
+
* when the package starts beside Sentry.
|
|
130
|
+
*/
|
|
131
|
+
exports.parentPolicy = { transparentForeign: false };
|
|
132
|
+
/**
|
|
133
|
+
* localParent is the span in ctx when it was started in this process
|
|
134
|
+
* (not a parent from an incoming header), and counts as one (see
|
|
135
|
+
* parentPolicy).
|
|
136
|
+
*/
|
|
137
|
+
function localParent(ctx) {
|
|
138
|
+
const span = api_1.trace.getSpan(ctx);
|
|
139
|
+
if (!span) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const spanContext = span.spanContext();
|
|
143
|
+
if (!api_1.trace.isSpanContextValid(spanContext) || spanContext.isRemote) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
if (exports.parentPolicy.transparentForeign && !recorded(span) && spanContext.traceState?.get(exports.droppedMark) === undefined) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return span;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* unsampled reports whether a span started under ctx would not be
|
|
153
|
+
* recorded (an unrecorded parent, or a root an entry wrapper decided
|
|
154
|
+
* against): the drivers then run untouched, without even a
|
|
155
|
+
* non-recording span or a context of their own.
|
|
156
|
+
*/
|
|
157
|
+
function unsampled(ctx) {
|
|
158
|
+
const local = localParent(ctx);
|
|
159
|
+
if (local) {
|
|
160
|
+
return !recorded(local);
|
|
161
|
+
}
|
|
162
|
+
return ctx.getValue(keys_js_1.rootDecisionKey) === false;
|
|
163
|
+
}
|
|
164
|
+
/** runCarried runs fn under the context carry returned, if any. */
|
|
165
|
+
function runCarried(ctx, fn) {
|
|
166
|
+
return ctx ? api_1.context.with(ctx, fn) : fn();
|
|
167
|
+
}
|
|
168
|
+
/** Attribute keys written. */
|
|
169
|
+
exports.codeFilepath = 'code.filepath';
|
|
170
|
+
exports.codeLineno = 'code.lineno';
|
|
171
|
+
exports.codeFunction = 'code.function';
|
|
172
|
+
/**
|
|
173
|
+
* eligible reports whether a span is a client operation worth a call
|
|
174
|
+
* site: client and producer spans, anything carrying db.system, and the
|
|
175
|
+
* operation spans of the ORMs that start their work in application code.
|
|
176
|
+
* A processor runs for recorded spans only, so every span here exports.
|
|
177
|
+
*/
|
|
178
|
+
function eligible(span) {
|
|
179
|
+
if (span.kind === api_1.SpanKind.CLIENT || span.kind === api_1.SpanKind.PRODUCER) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
if (isDatabase(span.attributes)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
return span.name.startsWith('drizzle.') || span.name.startsWith('prisma:client:');
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* clientOperationByOp reports whether a Sentry span is a client
|
|
189
|
+
* operation by its op, the one attribute Sentry sets when it starts a
|
|
190
|
+
* span (its postgres.js span gets db.system.name only after the start).
|
|
191
|
+
*/
|
|
192
|
+
function clientOperationByOp(span) {
|
|
193
|
+
const op = span.attributes['sentry.op'];
|
|
194
|
+
return typeof op === 'string' && /^(db|cache|http\.client|queue\.publish)\b/.test(op);
|
|
195
|
+
}
|
|
196
|
+
/** CallSiteProcessor implements SpanProcessor. */
|
|
197
|
+
class CallSiteProcessor {
|
|
198
|
+
sampling;
|
|
199
|
+
depth;
|
|
200
|
+
statement;
|
|
201
|
+
isApp;
|
|
202
|
+
/** above is where a processor capture starts (the SDK tracer startSpan); set once the provider exists. */
|
|
203
|
+
above;
|
|
204
|
+
readOnly;
|
|
205
|
+
keepHostKeys;
|
|
206
|
+
sites = new WeakMap();
|
|
207
|
+
/** captured counts spans given a call site (tests and the benchmark). */
|
|
208
|
+
captured = 0;
|
|
209
|
+
constructor(options) {
|
|
210
|
+
this.sampling = typeof options.sampling === 'number' ? new CallSiteSampling(options.sampling) : options.sampling;
|
|
211
|
+
this.depth = Math.max(1, Math.floor(options.depth));
|
|
212
|
+
this.statement = options.statement;
|
|
213
|
+
this.isApp = options.isApp ?? callsite_js_1.isAppFile;
|
|
214
|
+
this.above = options.above;
|
|
215
|
+
this.readOnly = options.readOnly === true;
|
|
216
|
+
this.keepHostKeys = options.keepHostKeys === true;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* assign gives a started span a call site found after its start (a
|
|
220
|
+
* driver's own origin, read once another SDK's span exists); it
|
|
221
|
+
* replaces any the processor found at the start.
|
|
222
|
+
*/
|
|
223
|
+
assign(span, site) {
|
|
224
|
+
if (!this.sites.has(span)) {
|
|
225
|
+
this.captured++;
|
|
226
|
+
}
|
|
227
|
+
this.sites.set(span, site);
|
|
228
|
+
if (!this.readOnly) {
|
|
229
|
+
stamp(span, site);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** siteOf is the call site the processor holds for a span. */
|
|
233
|
+
siteOf(span) {
|
|
234
|
+
return this.sites.get(span);
|
|
235
|
+
}
|
|
236
|
+
onStart(span, parentContext) {
|
|
237
|
+
try {
|
|
238
|
+
if (!this.readOnly) {
|
|
239
|
+
const changes = (0, privacy_js_1.scrubbed)(span.attributes, this.statement);
|
|
240
|
+
if (changes) {
|
|
241
|
+
span.setAttributes(changes);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!this.sampling.on || !(eligible(span) || (this.readOnly && clientOperationByOp(span)))) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const site = this.locate(parentContext);
|
|
248
|
+
if (!site) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this.sites.set(span, site);
|
|
252
|
+
this.captured++;
|
|
253
|
+
if (!this.readOnly) {
|
|
254
|
+
stamp(span, site);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// A call site is an extra; a failure to find one never reaches the app.
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* locate takes a site already carried in the context or by the parent
|
|
263
|
+
* span (an entry wrapper paid for it, under the same gate), else, one
|
|
264
|
+
* occasion in N, captures the synchronous stack.
|
|
265
|
+
*/
|
|
266
|
+
locate(parentContext) {
|
|
267
|
+
const carried = parentContext.getValue(keys_js_1.callSiteContextKey);
|
|
268
|
+
if (isLocation(carried)) {
|
|
269
|
+
return carried;
|
|
270
|
+
}
|
|
271
|
+
const parent = api_1.trace.getSpan(parentContext);
|
|
272
|
+
const inherited = parent ? this.sites.get(parent) : undefined;
|
|
273
|
+
if (inherited) {
|
|
274
|
+
return inherited;
|
|
275
|
+
}
|
|
276
|
+
if (!this.sampling.take()) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return (0, callsite_js_1.captureCallSite)(this.depth, this.isApp, this.above);
|
|
280
|
+
}
|
|
281
|
+
onEnd(span) {
|
|
282
|
+
if (this.readOnly) {
|
|
283
|
+
return; // exportView makes the copy the package sends
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
// The span is ended; the exporter reads these same objects, so the
|
|
287
|
+
// rules hold for attributes and events set after the start as well.
|
|
288
|
+
const attributes = span.attributes;
|
|
289
|
+
const changes = (0, privacy_js_1.scrubbed)(span.attributes, this.statement);
|
|
290
|
+
if (changes) {
|
|
291
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
292
|
+
attributes[key] = value;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (!this.keepHostKeys) {
|
|
296
|
+
routeFromHost(span.name, attributes);
|
|
297
|
+
}
|
|
298
|
+
const exceptionMessage = scrubEvents(span.events);
|
|
299
|
+
// A database span's status message is the server's text, which can
|
|
300
|
+
// quote the input that failed; the instrumentation's own exception
|
|
301
|
+
// message (the pg one says only the class and SQLSTATE) stands in.
|
|
302
|
+
if (span.status.code === api_1.SpanStatusCode.ERROR && isDatabase(span.attributes) && span.status.message) {
|
|
303
|
+
span.status.message = exceptionMessage ?? 'database error';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// never reaches the app
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* exportView is the span as the package exports it from a read-only
|
|
312
|
+
* processor: a copy of its attributes, events and status with the
|
|
313
|
+
* privacy rules and the call site applied, under the package's
|
|
314
|
+
* resource. The span itself is not touched.
|
|
315
|
+
*/
|
|
316
|
+
exportView(span, resource) {
|
|
317
|
+
const attributes = { ...span.attributes };
|
|
318
|
+
const changes = (0, privacy_js_1.scrubbed)(attributes, this.statement);
|
|
319
|
+
if (changes) {
|
|
320
|
+
Object.assign(attributes, changes);
|
|
321
|
+
}
|
|
322
|
+
const source = routeFromHost(span.name, attributes);
|
|
323
|
+
const site = this.sites.get(span);
|
|
324
|
+
if (site) {
|
|
325
|
+
attributes[exports.codeFilepath] = (0, callsite_js_1.displayPath)(site.file);
|
|
326
|
+
attributes[exports.codeLineno] = site.line;
|
|
327
|
+
if (site.function !== '') {
|
|
328
|
+
attributes[exports.codeFunction] = site.function;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const events = span.events.map((event) => ({ ...event, attributes: event.attributes ? { ...event.attributes } : undefined }));
|
|
332
|
+
const exceptionMessage = scrubEvents(events);
|
|
333
|
+
let status = span.status;
|
|
334
|
+
if (status.code === api_1.SpanStatusCode.ERROR && isDatabase(attributes) && status.message) {
|
|
335
|
+
status = { code: status.code, message: exceptionMessage ?? 'database error' };
|
|
336
|
+
}
|
|
337
|
+
// A database span another SDK named after its statement (Sentry's
|
|
338
|
+
// postgres.js span, whose own sanitizer misses $$ and E'' literals)
|
|
339
|
+
// is named as the statement attribute is kept.
|
|
340
|
+
const name = isDatabase(attributes) ? this.statementName(span.name) : exportedName(span.name, source);
|
|
341
|
+
return view(span, { name, attributes: attributes, events, status, ...(resource ? { resource } : {}) });
|
|
342
|
+
}
|
|
343
|
+
/** statementName is a statement-shaped span name under the statement rule. */
|
|
344
|
+
statementName(name) {
|
|
345
|
+
const bounded = name.length > privacy_js_1.maxStatementLength ? name.slice(0, privacy_js_1.maxStatementLength) : name;
|
|
346
|
+
return this.statement === 'off' ? (0, privacy_js_1.statementKeyword)(bounded) : (0, privacy_js_1.maskLiterals)(bounded);
|
|
347
|
+
}
|
|
348
|
+
shutdown() {
|
|
349
|
+
return Promise.resolve();
|
|
350
|
+
}
|
|
351
|
+
forceFlush() {
|
|
352
|
+
return Promise.resolve();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
exports.CallSiteProcessor = CallSiteProcessor;
|
|
356
|
+
/** stamp writes a call site on a span. */
|
|
357
|
+
function stamp(span, site) {
|
|
358
|
+
span.setAttribute(exports.codeFilepath, (0, callsite_js_1.displayPath)(site.file));
|
|
359
|
+
span.setAttribute(exports.codeLineno, site.line);
|
|
360
|
+
if (site.function !== '') {
|
|
361
|
+
span.setAttribute(exports.codeFunction, site.function);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* scrubEvents applies the stack rule to recorded exceptions (in place)
|
|
366
|
+
* and returns the last exception message. A recorded exception's stack
|
|
367
|
+
* opens with its message line, which a database server may fill with the
|
|
368
|
+
* input that failed; the frames alone say where it happened.
|
|
369
|
+
*/
|
|
370
|
+
function scrubEvents(events) {
|
|
371
|
+
let exceptionMessage;
|
|
372
|
+
for (const event of events) {
|
|
373
|
+
const attributes = event.attributes;
|
|
374
|
+
const stack = attributes?.['exception.stacktrace'];
|
|
375
|
+
if (typeof stack === 'string') {
|
|
376
|
+
attributes['exception.stacktrace'] = (0, privacy_js_1.framesOnly)(stack);
|
|
377
|
+
}
|
|
378
|
+
if (event.name === 'exception' && typeof attributes?.['exception.message'] === 'string') {
|
|
379
|
+
exceptionMessage = attributes['exception.message'];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return exceptionMessage;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* dropped reports the keys never exported: Sentry's bookkeeping
|
|
386
|
+
* (sentry.*), and request and response headers, which Sentry's http
|
|
387
|
+
* instrumentation records on its spans (cookies and authorization travel
|
|
388
|
+
* there) and the package's own never does.
|
|
389
|
+
*/
|
|
390
|
+
function dropped(key) {
|
|
391
|
+
const first = key.charCodeAt(0);
|
|
392
|
+
if (first === 115 /* s */) {
|
|
393
|
+
return key.startsWith('sentry.');
|
|
394
|
+
}
|
|
395
|
+
if (first === 99 /* c */) {
|
|
396
|
+
return key === 'cache.key'; // Sentry's cache spans: the keys
|
|
397
|
+
}
|
|
398
|
+
if (first === 114 /* r */) {
|
|
399
|
+
return key === 'rootcat.source'; // a source kept aside (see UrlSource)
|
|
400
|
+
}
|
|
401
|
+
return first === 104 /* h */ && (key.startsWith('http.request.header.') || key.startsWith('http.response.header.'));
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* routeFromHost drops the keys never exported and turns what a Sentry
|
|
405
|
+
* framework instrumentation wrote on the active span into the package's own
|
|
406
|
+
* vocabulary. @sentry/hono renames the server span to "GET /users/:id"
|
|
407
|
+
* once the route is known (sentry.source "route"): the template becomes
|
|
408
|
+
* http.route.
|
|
409
|
+
*/
|
|
410
|
+
function routeFromHost(name, attributes) {
|
|
411
|
+
let source;
|
|
412
|
+
let any = false;
|
|
413
|
+
for (const key in attributes) {
|
|
414
|
+
if (dropped(key)) {
|
|
415
|
+
any = true;
|
|
416
|
+
if (key === 'sentry.source' && source === undefined) {
|
|
417
|
+
source = attributes[key];
|
|
418
|
+
}
|
|
419
|
+
if (key === 'rootcat.source') {
|
|
420
|
+
source = attributes[key]; // what another SDK set, kept aside
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!any) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
for (const key of Object.keys(attributes)) {
|
|
428
|
+
if (dropped(key)) {
|
|
429
|
+
delete attributes[key];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (source === 'route' && attributes['http.route'] === undefined) {
|
|
433
|
+
const space = name.indexOf(' ');
|
|
434
|
+
if (space > 0 && name.charCodeAt(space + 1) === 47 /* / */) {
|
|
435
|
+
attributes['http.route'] = name.slice(space + 1, space + 1 + 256);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return source;
|
|
439
|
+
}
|
|
440
|
+
/** methodAndTarget matches a span name of an HTTP method and a path or URL. */
|
|
441
|
+
const methodAndTarget = /^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|CONNECT|TRACE) (\S+)$/;
|
|
442
|
+
/**
|
|
443
|
+
* exportedName is the name a span is exported under. Another SDK's HTTP
|
|
444
|
+
* span is named by the method and the path or URL it served or called
|
|
445
|
+
* (Sentry's: "GET /reset/3f9c...", "GET https://api.example.com/u/42"):
|
|
446
|
+
* the target is masked as an attribute URL is, unless the name is a
|
|
447
|
+
* route template a framework set (sentry.source "route").
|
|
448
|
+
*/
|
|
449
|
+
function exportedName(name, source) {
|
|
450
|
+
if (source === 'route') {
|
|
451
|
+
return name;
|
|
452
|
+
}
|
|
453
|
+
const match = methodAndTarget.exec(name);
|
|
454
|
+
return match ? `${match[1]} ${(0, privacy_js_1.maskPath)(match[2])}` : name;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* view is a ReadableSpan with the span's fields except the given ones,
|
|
458
|
+
* and always flagged sampled: the package exports only spans it chose to
|
|
459
|
+
* record, whatever flag they carry inside the process.
|
|
460
|
+
*/
|
|
461
|
+
function view(span, overrides = {}) {
|
|
462
|
+
const spanContext = span.spanContext();
|
|
463
|
+
const flagged = (spanContext.traceFlags & api_1.TraceFlags.SAMPLED) !== 0 ? spanContext : { ...spanContext, traceFlags: spanContext.traceFlags | api_1.TraceFlags.SAMPLED };
|
|
464
|
+
return {
|
|
465
|
+
name: overrides.name ?? span.name,
|
|
466
|
+
kind: span.kind,
|
|
467
|
+
spanContext: () => flagged,
|
|
468
|
+
parentSpanContext: span.parentSpanContext,
|
|
469
|
+
startTime: span.startTime,
|
|
470
|
+
endTime: span.endTime,
|
|
471
|
+
status: overrides.status ?? span.status,
|
|
472
|
+
attributes: overrides.attributes ?? span.attributes,
|
|
473
|
+
links: span.links,
|
|
474
|
+
events: overrides.events ?? span.events,
|
|
475
|
+
duration: span.duration,
|
|
476
|
+
ended: span.ended,
|
|
477
|
+
resource: overrides.resource ?? span.resource,
|
|
478
|
+
instrumentationScope: span.instrumentationScope,
|
|
479
|
+
droppedAttributesCount: span.droppedAttributesCount,
|
|
480
|
+
droppedEventsCount: span.droppedEventsCount,
|
|
481
|
+
droppedLinksCount: span.droppedLinksCount,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function isDatabase(attributes) {
|
|
485
|
+
return attributes['db.system'] !== undefined || attributes['db.system.name'] !== undefined;
|
|
486
|
+
}
|
|
487
|
+
function isLocation(value) {
|
|
488
|
+
if (!value || typeof value !== 'object') {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
const record = value;
|
|
492
|
+
return typeof record.file === 'string' && typeof record.line === 'number';
|
|
493
|
+
}
|
|
494
|
+
//# sourceMappingURL=processor.js.map
|