@statoss/sdk 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +411 -2
- package/dist/cjs/client.d.ts +50 -0
- package/dist/cjs/client.js +462 -0
- package/dist/cjs/dsn.d.ts +25 -0
- package/dist/cjs/dsn.js +62 -0
- package/dist/cjs/envelope.d.ts +36 -0
- package/dist/cjs/envelope.js +55 -0
- package/dist/cjs/event.d.ts +9 -0
- package/dist/cjs/event.js +128 -0
- package/dist/cjs/hub.d.ts +37 -0
- package/dist/cjs/hub.js +119 -0
- package/dist/cjs/index.d.ts +38 -0
- package/dist/cjs/index.js +107 -0
- package/dist/cjs/integrations/browser.d.ts +7 -0
- package/dist/cjs/integrations/browser.js +124 -0
- package/dist/cjs/integrations/console.d.ts +3 -0
- package/dist/cjs/integrations/console.js +106 -0
- package/dist/cjs/integrations/dedupe.d.ts +7 -0
- package/dist/cjs/integrations/dedupe.js +32 -0
- package/dist/cjs/integrations/fetch.d.ts +3 -0
- package/dist/cjs/integrations/fetch.js +69 -0
- package/dist/cjs/integrations/handlers.d.ts +9 -0
- package/dist/cjs/integrations/handlers.js +192 -0
- package/dist/cjs/levels.d.ts +20 -0
- package/dist/cjs/levels.js +77 -0
- package/dist/cjs/logger.d.ts +18 -0
- package/dist/cjs/logger.js +38 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/ratelimit.d.ts +11 -0
- package/dist/cjs/ratelimit.js +50 -0
- package/dist/cjs/runtime.d.ts +42 -0
- package/dist/cjs/runtime.js +188 -0
- package/dist/cjs/scope.d.ts +30 -0
- package/dist/cjs/scope.js +127 -0
- package/dist/cjs/stack.d.ts +9 -0
- package/dist/cjs/stack.js +129 -0
- package/dist/cjs/transport.d.ts +40 -0
- package/dist/cjs/transport.js +218 -0
- package/dist/cjs/types.d.ts +180 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/utils.d.ts +27 -0
- package/dist/cjs/utils.js +227 -0
- package/dist/cjs/version.d.ts +3 -0
- package/dist/cjs/version.js +7 -0
- package/dist/esm/client.d.ts +50 -0
- package/dist/esm/client.js +458 -0
- package/dist/esm/dsn.d.ts +25 -0
- package/dist/esm/dsn.js +55 -0
- package/dist/esm/envelope.d.ts +36 -0
- package/dist/esm/envelope.js +49 -0
- package/dist/esm/event.d.ts +9 -0
- package/dist/esm/event.js +124 -0
- package/dist/esm/hub.d.ts +37 -0
- package/dist/esm/hub.js +111 -0
- package/dist/esm/index.d.ts +38 -0
- package/dist/esm/index.js +88 -0
- package/dist/esm/integrations/browser.d.ts +7 -0
- package/dist/esm/integrations/browser.js +119 -0
- package/dist/esm/integrations/console.d.ts +3 -0
- package/dist/esm/integrations/console.js +103 -0
- package/dist/esm/integrations/dedupe.d.ts +7 -0
- package/dist/esm/integrations/dedupe.js +29 -0
- package/dist/esm/integrations/fetch.d.ts +3 -0
- package/dist/esm/integrations/fetch.js +66 -0
- package/dist/esm/integrations/handlers.d.ts +9 -0
- package/dist/esm/integrations/handlers.js +188 -0
- package/dist/esm/levels.d.ts +20 -0
- package/dist/esm/levels.js +69 -0
- package/dist/esm/logger.d.ts +18 -0
- package/dist/esm/logger.js +33 -0
- package/dist/esm/ratelimit.d.ts +11 -0
- package/dist/esm/ratelimit.js +45 -0
- package/dist/esm/runtime.d.ts +42 -0
- package/dist/esm/runtime.js +181 -0
- package/dist/esm/scope.d.ts +30 -0
- package/dist/esm/scope.js +123 -0
- package/dist/esm/stack.d.ts +9 -0
- package/dist/esm/stack.js +125 -0
- package/dist/esm/transport.d.ts +40 -0
- package/dist/esm/transport.js +213 -0
- package/dist/esm/types.d.ts +180 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/utils.d.ts +27 -0
- package/dist/esm/utils.js +211 -0
- package/dist/esm/version.d.ts +3 -0
- package/dist/esm/version.js +4 -0
- package/package.json +48 -4
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Logger } from "./logger.js";
|
|
2
|
+
import { type RuntimeInfo } from "./runtime.js";
|
|
3
|
+
import { type Scope } from "./scope.js";
|
|
4
|
+
import type { Breadcrumb, BreadcrumbHint, CaptureContext, Event, InitOptions } from "./types.js";
|
|
5
|
+
export declare class Client {
|
|
6
|
+
readonly logger: Logger;
|
|
7
|
+
readonly runtime: RuntimeInfo;
|
|
8
|
+
readonly enabled: boolean;
|
|
9
|
+
readonly integrations: string[];
|
|
10
|
+
private readonly options;
|
|
11
|
+
private readonly dsn?;
|
|
12
|
+
private readonly endpoint?;
|
|
13
|
+
private readonly sender?;
|
|
14
|
+
private readonly levels;
|
|
15
|
+
private readonly sampleRate;
|
|
16
|
+
private readonly maxBreadcrumbs;
|
|
17
|
+
private readonly discarded;
|
|
18
|
+
private readonly pending;
|
|
19
|
+
private readonly teardowns;
|
|
20
|
+
private reportTimer?;
|
|
21
|
+
private previousSignature?;
|
|
22
|
+
private closed;
|
|
23
|
+
constructor(options: InitOptions);
|
|
24
|
+
/** Installs the integrations that apply to this runtime. */
|
|
25
|
+
setup(): void;
|
|
26
|
+
captureException(error: unknown, context: CaptureContext | undefined, scope: Scope): string;
|
|
27
|
+
captureMessage(message: unknown, context: CaptureContext | undefined, scope: Scope): string;
|
|
28
|
+
captureEvent(event: Event, context: CaptureContext | undefined, scope: Scope): string;
|
|
29
|
+
/** Applies beforeBreadcrumb and keeps the breadcrumb on the current scope. */
|
|
30
|
+
addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void;
|
|
31
|
+
/** True for a URL this client sends envelopes to, so the fetch breadcrumbs skip it. */
|
|
32
|
+
isOwnUrl(url: string): boolean;
|
|
33
|
+
/** Counts something not sent, for the next client report. */
|
|
34
|
+
recordLoss(reason: string, category: string, quantity?: number): void;
|
|
35
|
+
sendClientReport(): void;
|
|
36
|
+
/** Waits for captured events to be sent; false if timeoutMs passed first. */
|
|
37
|
+
flush(timeoutMs?: number): Promise<boolean>;
|
|
38
|
+
/** Flushes, then removes the integrations. Nothing is captured afterwards. */
|
|
39
|
+
close(timeoutMs?: number): Promise<boolean>;
|
|
40
|
+
/** Removes the integrations at once, without waiting. */
|
|
41
|
+
teardown(): void;
|
|
42
|
+
private flushReportsWhenHidden;
|
|
43
|
+
private capture;
|
|
44
|
+
/** Merges scope, context and options into the event, in Sentry's field order. */
|
|
45
|
+
private prepare;
|
|
46
|
+
private process;
|
|
47
|
+
/** Makes what beforeSend handed back valid again: a Sentry level, string tags, a sane size. */
|
|
48
|
+
private finish;
|
|
49
|
+
private send;
|
|
50
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { authHeader, envelopeEndpoint, envelopeUrlWithQueryAuth, parseDsn, } from "./dsn.js";
|
|
2
|
+
import { clientReportEnvelope, eventEnvelope, } from "./envelope.js";
|
|
3
|
+
import { addInitialFrame, eventFromUnknown } from "./event.js";
|
|
4
|
+
import { getCurrentScope } from "./hub.js";
|
|
5
|
+
import { installClicks, installNavigation } from "./integrations/browser.js";
|
|
6
|
+
import { installConsole } from "./integrations/console.js";
|
|
7
|
+
import { eventSignature } from "./integrations/dedupe.js";
|
|
8
|
+
import { installFetch } from "./integrations/fetch.js";
|
|
9
|
+
import { installGlobalHandlers } from "./integrations/handlers.js";
|
|
10
|
+
import { CATEGORY_TAG, chooseLevel, isSentryLevel, LEVEL_TAG, readCategory, readLevels, } from "./levels.js";
|
|
11
|
+
import { makeLogger } from "./logger.js";
|
|
12
|
+
import { describeRuntime } from "./runtime.js";
|
|
13
|
+
import { MAX_BREADCRUMBS } from "./scope.js";
|
|
14
|
+
import { fetchTransport, Sender } from "./transport.js";
|
|
15
|
+
import { isObject, isThenable, normalize, now, safely, safeString, scrubUrl, truncate, unref, uuid4, } from "./utils.js";
|
|
16
|
+
import { SDK_NAME, SDK_PACKAGE, SDK_VERSION } from "./version.js";
|
|
17
|
+
const REPORT_INTERVAL_MS = 30000;
|
|
18
|
+
const DEFAULT_FLUSH_MS = 2000;
|
|
19
|
+
// The server drops event items over 1 MiB.
|
|
20
|
+
const MAX_EVENT_CHARS = 1000000;
|
|
21
|
+
const MAX_TAG_VALUE = 200;
|
|
22
|
+
const KNOWN_KEYS = new Set([
|
|
23
|
+
"event_id",
|
|
24
|
+
"timestamp",
|
|
25
|
+
"platform",
|
|
26
|
+
"level",
|
|
27
|
+
"logger",
|
|
28
|
+
"transaction",
|
|
29
|
+
"server_name",
|
|
30
|
+
"environment",
|
|
31
|
+
"release",
|
|
32
|
+
"dist",
|
|
33
|
+
"message",
|
|
34
|
+
"exception",
|
|
35
|
+
"breadcrumbs",
|
|
36
|
+
"tags",
|
|
37
|
+
"extra",
|
|
38
|
+
"contexts",
|
|
39
|
+
"user",
|
|
40
|
+
"request",
|
|
41
|
+
"fingerprint",
|
|
42
|
+
"sdk",
|
|
43
|
+
]);
|
|
44
|
+
function cleanTags(tags) {
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
47
|
+
if (value === undefined || value === null)
|
|
48
|
+
continue;
|
|
49
|
+
out[key] = truncate(typeof value === "string" ? value : safeString(value), MAX_TAG_VALUE);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function cleanContexts(contexts) {
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const [name, value] of Object.entries(contexts)) {
|
|
56
|
+
if (isObject(value))
|
|
57
|
+
out[name] = normalize(value, 5);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function pageRequest() {
|
|
62
|
+
const g = globalThis;
|
|
63
|
+
const href = safely(() => g.location?.href);
|
|
64
|
+
const ua = safely(() => g.navigator?.userAgent);
|
|
65
|
+
const request = {};
|
|
66
|
+
if (typeof href === "string")
|
|
67
|
+
request.url = scrubUrl(href);
|
|
68
|
+
if (typeof ua === "string")
|
|
69
|
+
request.headers = { "User-Agent": ua };
|
|
70
|
+
return Object.keys(request).length ? request : undefined;
|
|
71
|
+
}
|
|
72
|
+
function crumbsOf(value) {
|
|
73
|
+
if (Array.isArray(value))
|
|
74
|
+
return value;
|
|
75
|
+
if (isObject(value) && Array.isArray(value.values))
|
|
76
|
+
return value.values;
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
function withTimeout(promise, ms) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const timer = setTimeout(() => resolve(false), ms);
|
|
82
|
+
unref(timer);
|
|
83
|
+
promise.then(() => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
resolve(true);
|
|
86
|
+
}, () => {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
resolve(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export class Client {
|
|
93
|
+
constructor(options) {
|
|
94
|
+
this.integrations = [];
|
|
95
|
+
this.discarded = new Map();
|
|
96
|
+
this.pending = new Set();
|
|
97
|
+
this.teardowns = [];
|
|
98
|
+
this.closed = false;
|
|
99
|
+
this.options = options;
|
|
100
|
+
this.logger = makeLogger(options.debug === true);
|
|
101
|
+
this.runtime = describeRuntime();
|
|
102
|
+
this.levels = readLevels(options.levels, this.logger);
|
|
103
|
+
const rate = options.sampleRate;
|
|
104
|
+
if (rate !== undefined &&
|
|
105
|
+
!(typeof rate === "number" && rate >= 0 && rate <= 1)) {
|
|
106
|
+
this.logger.warn("Ignored sampleRate: it should be a number from 0 to 1.");
|
|
107
|
+
}
|
|
108
|
+
this.sampleRate =
|
|
109
|
+
typeof rate === "number" && rate >= 0 && rate <= 1 ? rate : 1;
|
|
110
|
+
const max = options.maxBreadcrumbs;
|
|
111
|
+
this.maxBreadcrumbs =
|
|
112
|
+
typeof max === "number" && max >= 0
|
|
113
|
+
? Math.min(Math.floor(max), MAX_BREADCRUMBS)
|
|
114
|
+
: MAX_BREADCRUMBS;
|
|
115
|
+
let dsn;
|
|
116
|
+
if (options.enabled === false) {
|
|
117
|
+
this.logger.log("Not started: enabled is false.");
|
|
118
|
+
}
|
|
119
|
+
else if (options.dsn === undefined || options.dsn === "") {
|
|
120
|
+
this.logger.warn("Not started: no DSN given.");
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const parsed = parseDsn(options.dsn);
|
|
124
|
+
if (parsed.error !== undefined)
|
|
125
|
+
this.logger.warn(`Not started: ${parsed.error}`);
|
|
126
|
+
dsn = parsed.dsn;
|
|
127
|
+
}
|
|
128
|
+
this.dsn = dsn;
|
|
129
|
+
this.enabled = dsn !== undefined;
|
|
130
|
+
if (!dsn)
|
|
131
|
+
return;
|
|
132
|
+
this.endpoint = envelopeEndpoint(dsn);
|
|
133
|
+
const browserLike = this.runtime.browserLike;
|
|
134
|
+
this.sender = new Sender({
|
|
135
|
+
url: browserLike ? envelopeUrlWithQueryAuth(dsn) : this.endpoint,
|
|
136
|
+
headers: browserLike
|
|
137
|
+
? { "Content-Type": "text/plain;charset=UTF-8" }
|
|
138
|
+
: {
|
|
139
|
+
"Content-Type": "application/x-sentry-envelope",
|
|
140
|
+
"X-Sentry-Auth": authHeader(dsn),
|
|
141
|
+
},
|
|
142
|
+
browserLike,
|
|
143
|
+
transport: options.transport ?? fetchTransport(browserLike),
|
|
144
|
+
recordLoss: (reason, category, quantity) => this.recordLoss(reason, category, quantity),
|
|
145
|
+
logger: this.logger,
|
|
146
|
+
});
|
|
147
|
+
this.logger.log(`Started for app ${dsn.projectId} on ${dsn.host} (${this.runtime.name}, ${SDK_NAME} ${SDK_VERSION}).`);
|
|
148
|
+
}
|
|
149
|
+
/** Installs the integrations that apply to this runtime. */
|
|
150
|
+
setup() {
|
|
151
|
+
if (!this.enabled)
|
|
152
|
+
return;
|
|
153
|
+
const toggles = {
|
|
154
|
+
globalHandlers: true,
|
|
155
|
+
console: true,
|
|
156
|
+
fetch: true,
|
|
157
|
+
navigation: true,
|
|
158
|
+
clicks: true,
|
|
159
|
+
dedupe: true,
|
|
160
|
+
...this.options.integrations,
|
|
161
|
+
};
|
|
162
|
+
const add = (name, on, install) => {
|
|
163
|
+
if (!on)
|
|
164
|
+
return;
|
|
165
|
+
const teardown = safely(install);
|
|
166
|
+
if (!teardown)
|
|
167
|
+
return;
|
|
168
|
+
this.integrations.push(name);
|
|
169
|
+
this.teardowns.push(teardown);
|
|
170
|
+
};
|
|
171
|
+
const browser = this.runtime.name === "browser";
|
|
172
|
+
add("GlobalHandlers", toggles.globalHandlers, () => installGlobalHandlers(this));
|
|
173
|
+
add("Console", toggles.console, () => installConsole(this));
|
|
174
|
+
add("Fetch", toggles.fetch, () => installFetch(this));
|
|
175
|
+
add("Navigation", browser && toggles.navigation, () => installNavigation(this));
|
|
176
|
+
add("Clicks", browser && toggles.clicks, () => installClicks(this));
|
|
177
|
+
if (toggles.dedupe)
|
|
178
|
+
this.integrations.push("Dedupe");
|
|
179
|
+
if (browser)
|
|
180
|
+
this.flushReportsWhenHidden();
|
|
181
|
+
this.logger.log(`Integrations: ${this.integrations.join(", ") || "none"}.`);
|
|
182
|
+
}
|
|
183
|
+
captureException(error, context, scope) {
|
|
184
|
+
return this.capture(() => eventFromUnknown(error, { type: "generic", handled: true }), context, scope, { originalException: error });
|
|
185
|
+
}
|
|
186
|
+
captureMessage(message, context, scope) {
|
|
187
|
+
return this.capture(() => ({ message: truncate(safeString(message)), level: "info" }), context, scope, {});
|
|
188
|
+
}
|
|
189
|
+
captureEvent(event, context, scope) {
|
|
190
|
+
const id = isObject(event) && typeof event.event_id === "string"
|
|
191
|
+
? event.event_id
|
|
192
|
+
: "";
|
|
193
|
+
return this.capture(() => ({ ...event }), context, scope, {
|
|
194
|
+
eventId: /^[0-9a-f]{32}$/.test(id) ? id : undefined,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/** @internal Used by the global handlers. */
|
|
198
|
+
captureUnhandled(input, mechanism, level, options = {}) {
|
|
199
|
+
return this.capture(() => {
|
|
200
|
+
const event = eventFromUnknown(input, mechanism, options.rejection);
|
|
201
|
+
return options.frame ? addInitialFrame(event, options.frame) : event;
|
|
202
|
+
}, undefined, getCurrentScope(), { originalException: input, level });
|
|
203
|
+
}
|
|
204
|
+
/** Applies beforeBreadcrumb and keeps the breadcrumb on the current scope. */
|
|
205
|
+
addBreadcrumb(breadcrumb, hint) {
|
|
206
|
+
if (!this.enabled ||
|
|
207
|
+
this.closed ||
|
|
208
|
+
this.maxBreadcrumbs === 0 ||
|
|
209
|
+
!isObject(breadcrumb))
|
|
210
|
+
return;
|
|
211
|
+
try {
|
|
212
|
+
let crumb = { timestamp: now(), ...breadcrumb };
|
|
213
|
+
if (crumb.message !== undefined)
|
|
214
|
+
crumb.message = truncate(safeString(crumb.message), 1024);
|
|
215
|
+
if (crumb.data !== undefined) {
|
|
216
|
+
crumb.data = normalize(crumb.data, 3, 1024);
|
|
217
|
+
}
|
|
218
|
+
if (this.options.beforeBreadcrumb)
|
|
219
|
+
crumb = this.options.beforeBreadcrumb(crumb, hint);
|
|
220
|
+
if (!crumb)
|
|
221
|
+
return;
|
|
222
|
+
const scope = getCurrentScope();
|
|
223
|
+
scope._maxBreadcrumbs = this.maxBreadcrumbs;
|
|
224
|
+
scope.addBreadcrumb(crumb);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
this.logger.error("Could not add a breadcrumb:", error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** True for a URL this client sends envelopes to, so the fetch breadcrumbs skip it. */
|
|
231
|
+
isOwnUrl(url) {
|
|
232
|
+
return this.endpoint !== undefined && url.startsWith(this.endpoint);
|
|
233
|
+
}
|
|
234
|
+
/** Counts something not sent, for the next client report. */
|
|
235
|
+
recordLoss(reason, category, quantity = 1) {
|
|
236
|
+
const key = `${reason}:${category}`;
|
|
237
|
+
const entry = this.discarded.get(key);
|
|
238
|
+
if (entry)
|
|
239
|
+
entry.quantity += quantity;
|
|
240
|
+
else
|
|
241
|
+
this.discarded.set(key, { reason, category, quantity });
|
|
242
|
+
if (this.reportTimer === undefined && this.sender) {
|
|
243
|
+
this.reportTimer = safely(() => setTimeout(() => {
|
|
244
|
+
this.reportTimer = undefined;
|
|
245
|
+
this.sendClientReport();
|
|
246
|
+
}, REPORT_INTERVAL_MS));
|
|
247
|
+
unref(this.reportTimer);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
sendClientReport() {
|
|
251
|
+
if (this.reportTimer !== undefined) {
|
|
252
|
+
clearTimeout(this.reportTimer);
|
|
253
|
+
this.reportTimer = undefined;
|
|
254
|
+
}
|
|
255
|
+
if (!this.discarded.size || !this.sender || !this.dsn)
|
|
256
|
+
return;
|
|
257
|
+
const discarded = [...this.discarded.values()];
|
|
258
|
+
this.discarded.clear();
|
|
259
|
+
this.sender.send(clientReportEnvelope(discarded, this.dsn));
|
|
260
|
+
}
|
|
261
|
+
/** Waits for captured events to be sent; false if timeoutMs passed first. */
|
|
262
|
+
async flush(timeoutMs = DEFAULT_FLUSH_MS) {
|
|
263
|
+
if (!this.sender)
|
|
264
|
+
return true;
|
|
265
|
+
const deadline = Date.now() + timeoutMs;
|
|
266
|
+
const left = () => Math.max(0, deadline - Date.now());
|
|
267
|
+
const processed = this.pending.size
|
|
268
|
+
? await withTimeout(Promise.all([...this.pending]), timeoutMs)
|
|
269
|
+
: true;
|
|
270
|
+
this.sendClientReport();
|
|
271
|
+
let sent = await this.sender.flush(left());
|
|
272
|
+
// Report what this flush itself lost, such as events that hit a network error.
|
|
273
|
+
if (this.discarded.size) {
|
|
274
|
+
this.sendClientReport();
|
|
275
|
+
sent = (await this.sender.flush(left())) && sent;
|
|
276
|
+
}
|
|
277
|
+
return processed && sent;
|
|
278
|
+
}
|
|
279
|
+
/** Flushes, then removes the integrations. Nothing is captured afterwards. */
|
|
280
|
+
async close(timeoutMs = DEFAULT_FLUSH_MS) {
|
|
281
|
+
this.closed = true;
|
|
282
|
+
this.teardown();
|
|
283
|
+
return this.flush(timeoutMs);
|
|
284
|
+
}
|
|
285
|
+
/** Removes the integrations at once, without waiting. */
|
|
286
|
+
teardown() {
|
|
287
|
+
for (const teardown of this.teardowns.splice(0))
|
|
288
|
+
safely(teardown);
|
|
289
|
+
}
|
|
290
|
+
flushReportsWhenHidden() {
|
|
291
|
+
const doc = globalThis.document;
|
|
292
|
+
if (!doc || typeof doc.addEventListener !== "function")
|
|
293
|
+
return;
|
|
294
|
+
const onChange = () => {
|
|
295
|
+
if (doc.visibilityState === "hidden")
|
|
296
|
+
safely(() => this.sendClientReport());
|
|
297
|
+
};
|
|
298
|
+
doc.addEventListener("visibilitychange", onChange);
|
|
299
|
+
this.teardowns.push(() => doc.removeEventListener?.("visibilitychange", onChange));
|
|
300
|
+
}
|
|
301
|
+
capture(build, context, scope, hint) {
|
|
302
|
+
const eventId = hint.eventId ?? uuid4();
|
|
303
|
+
if (!this.enabled || this.closed)
|
|
304
|
+
return eventId;
|
|
305
|
+
try {
|
|
306
|
+
const event = this.prepare(build(), context, scope, eventId, hint.level);
|
|
307
|
+
const task = this.process(event, {
|
|
308
|
+
event_id: eventId,
|
|
309
|
+
originalException: hint.originalException,
|
|
310
|
+
}).catch((error) => {
|
|
311
|
+
this.logger.error("Could not send an event:", error);
|
|
312
|
+
});
|
|
313
|
+
this.pending.add(task);
|
|
314
|
+
void task.then(() => this.pending.delete(task));
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
this.logger.error("Could not capture an event:", error);
|
|
318
|
+
}
|
|
319
|
+
return eventId;
|
|
320
|
+
}
|
|
321
|
+
/** Merges scope, context and options into the event, in Sentry's field order. */
|
|
322
|
+
prepare(base, context, scope, eventId, forcedLevel) {
|
|
323
|
+
const ctx = isObject(context) ? context : {};
|
|
324
|
+
const choice = chooseLevel([ctx.level, forcedLevel, scope._level, base.level], this.levels, "error", this.logger);
|
|
325
|
+
const tags = {
|
|
326
|
+
...scope._tags,
|
|
327
|
+
...ctx.tags,
|
|
328
|
+
...base.tags,
|
|
329
|
+
};
|
|
330
|
+
if (choice.custom)
|
|
331
|
+
tags[LEVEL_TAG] = choice.custom;
|
|
332
|
+
const category = readCategory(ctx.category, this.logger);
|
|
333
|
+
if (category)
|
|
334
|
+
tags[CATEGORY_TAG] = category;
|
|
335
|
+
const user = {
|
|
336
|
+
...scope._user,
|
|
337
|
+
...ctx.user,
|
|
338
|
+
...base.user,
|
|
339
|
+
};
|
|
340
|
+
if (this.options.sendDefaultPii &&
|
|
341
|
+
this.runtime.browserLike &&
|
|
342
|
+
user.ip_address === undefined) {
|
|
343
|
+
user.ip_address = "{{auto}}";
|
|
344
|
+
}
|
|
345
|
+
const request = this.runtime.browserLike
|
|
346
|
+
? { ...pageRequest(), ...base.request }
|
|
347
|
+
: base.request;
|
|
348
|
+
const fingerprint = base.fingerprint ?? ctx.fingerprint ?? scope._fingerprint;
|
|
349
|
+
const allCrumbs = [...scope._breadcrumbs, ...crumbsOf(base.breadcrumbs)];
|
|
350
|
+
const breadcrumbs = allCrumbs.slice(Math.max(0, allCrumbs.length - this.maxBreadcrumbs));
|
|
351
|
+
const event = {
|
|
352
|
+
event_id: eventId,
|
|
353
|
+
timestamp: typeof base.timestamp === "number" ? base.timestamp : now(),
|
|
354
|
+
platform: base.platform ?? this.runtime.platform,
|
|
355
|
+
level: choice.level,
|
|
356
|
+
};
|
|
357
|
+
const set = (key, value) => {
|
|
358
|
+
if (value === undefined)
|
|
359
|
+
return;
|
|
360
|
+
if (isObject(value) &&
|
|
361
|
+
!Array.isArray(value) &&
|
|
362
|
+
Object.keys(value).length === 0)
|
|
363
|
+
return;
|
|
364
|
+
event[key] = value;
|
|
365
|
+
};
|
|
366
|
+
set("logger", base.logger);
|
|
367
|
+
set("transaction", base.transaction);
|
|
368
|
+
set("server_name", base.server_name ?? this.options.serverName ?? this.runtime.serverName);
|
|
369
|
+
set("environment", base.environment ?? this.options.environment ?? "production");
|
|
370
|
+
set("release", base.release ?? this.options.release);
|
|
371
|
+
set("dist", base.dist ?? this.options.dist);
|
|
372
|
+
set("message", base.message);
|
|
373
|
+
set("exception", base.exception);
|
|
374
|
+
set("breadcrumbs", breadcrumbs.length ? { values: breadcrumbs } : undefined);
|
|
375
|
+
set("tags", cleanTags(tags));
|
|
376
|
+
set("extra", normalize({ ...scope._extra, ...ctx.extra, ...base.extra }, 6));
|
|
377
|
+
set("contexts", cleanContexts({
|
|
378
|
+
...this.runtime.contexts,
|
|
379
|
+
...scope._contexts,
|
|
380
|
+
...ctx.contexts,
|
|
381
|
+
...base.contexts,
|
|
382
|
+
}));
|
|
383
|
+
set("user", normalize(user, 3));
|
|
384
|
+
set("request", request);
|
|
385
|
+
set("fingerprint", Array.isArray(fingerprint)
|
|
386
|
+
? fingerprint.map((f) => safeString(f))
|
|
387
|
+
: undefined);
|
|
388
|
+
set("sdk", {
|
|
389
|
+
name: SDK_NAME,
|
|
390
|
+
version: SDK_VERSION,
|
|
391
|
+
integrations: [...this.integrations],
|
|
392
|
+
packages: [{ name: SDK_PACKAGE, version: SDK_VERSION }],
|
|
393
|
+
});
|
|
394
|
+
for (const key of Object.keys(base)) {
|
|
395
|
+
if (!KNOWN_KEYS.has(key) && base[key] !== undefined)
|
|
396
|
+
event[key] = base[key];
|
|
397
|
+
}
|
|
398
|
+
return event;
|
|
399
|
+
}
|
|
400
|
+
async process(event, hint) {
|
|
401
|
+
if (this.sampleRate < 1 && Math.random() >= this.sampleRate) {
|
|
402
|
+
this.logger.log("Dropped an event by sampleRate.");
|
|
403
|
+
this.recordLoss("sample_rate", "error");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (this.integrations.includes("Dedupe")) {
|
|
407
|
+
const signature = eventSignature(event);
|
|
408
|
+
if (signature !== undefined && signature === this.previousSignature) {
|
|
409
|
+
this.logger.log("Dropped an event identical to the one before it.");
|
|
410
|
+
this.recordLoss("event_processor", "error");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
this.previousSignature = signature;
|
|
414
|
+
}
|
|
415
|
+
let final = event;
|
|
416
|
+
if (this.options.beforeSend) {
|
|
417
|
+
try {
|
|
418
|
+
const result = this.options.beforeSend(event, hint);
|
|
419
|
+
final = isThenable(result) ? await result : result;
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
this.logger.error("beforeSend threw, so the event was dropped:", error);
|
|
423
|
+
this.recordLoss("internal_sdk_error", "error");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (!isObject(final)) {
|
|
427
|
+
this.logger.log("beforeSend dropped an event.");
|
|
428
|
+
this.recordLoss("before_send", "error");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
this.send(this.finish(final));
|
|
433
|
+
}
|
|
434
|
+
/** Makes what beforeSend handed back valid again: a Sentry level, string tags, a sane size. */
|
|
435
|
+
finish(event) {
|
|
436
|
+
if (!isSentryLevel(event.level)) {
|
|
437
|
+
const choice = chooseLevel([event.level], this.levels, "error", this.logger);
|
|
438
|
+
event.level = choice.level;
|
|
439
|
+
if (choice.custom)
|
|
440
|
+
event.tags = { ...event.tags, [LEVEL_TAG]: choice.custom };
|
|
441
|
+
}
|
|
442
|
+
if (isObject(event.tags))
|
|
443
|
+
event.tags = cleanTags(event.tags);
|
|
444
|
+
const size = () => safely(() => JSON.stringify(event).length, 0) ?? 0;
|
|
445
|
+
if (size() > MAX_EVENT_CHARS) {
|
|
446
|
+
this.logger.warn("The event is over 1 MB; sending it without breadcrumbs and extra.");
|
|
447
|
+
delete event.breadcrumbs;
|
|
448
|
+
if (size() > MAX_EVENT_CHARS)
|
|
449
|
+
delete event.extra;
|
|
450
|
+
}
|
|
451
|
+
return event;
|
|
452
|
+
}
|
|
453
|
+
send(event) {
|
|
454
|
+
if (!this.sender || !this.dsn)
|
|
455
|
+
return;
|
|
456
|
+
this.sender.send(eventEnvelope(event, this.dsn));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface Dsn {
|
|
2
|
+
protocol: "http" | "https";
|
|
3
|
+
publicKey: string;
|
|
4
|
+
host: string;
|
|
5
|
+
port: string;
|
|
6
|
+
/** Anything between the host and the app number, without slashes at the ends. */
|
|
7
|
+
path: string;
|
|
8
|
+
/** The app number (Sentry's project id). */
|
|
9
|
+
projectId: string;
|
|
10
|
+
}
|
|
11
|
+
export type DsnResult = {
|
|
12
|
+
dsn: Dsn;
|
|
13
|
+
error?: undefined;
|
|
14
|
+
} | {
|
|
15
|
+
dsn?: undefined;
|
|
16
|
+
error: string;
|
|
17
|
+
};
|
|
18
|
+
/** Parses a DSN; a bad one comes back as a reason, never as an exception. */
|
|
19
|
+
export declare function parseDsn(input: unknown): DsnResult;
|
|
20
|
+
/** The DSN without any secret part, as Sentry writes it in envelope headers. */
|
|
21
|
+
export declare function dsnToString(dsn: Dsn): string;
|
|
22
|
+
export declare function envelopeEndpoint(dsn: Dsn): string;
|
|
23
|
+
/** The endpoint with the key in the query, so browsers send without a CORS preflight. */
|
|
24
|
+
export declare function envelopeUrlWithQueryAuth(dsn: Dsn): string;
|
|
25
|
+
export declare function authHeader(dsn: Dsn): string;
|
package/dist/esm/dsn.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SDK_NAME, SDK_VERSION } from "./version.js";
|
|
2
|
+
const DSN_RE = /^(https?):\/\/([^:@/?#\s]+)(?::[^@/?#\s]*)?@(\[[0-9a-fA-F:.]+\]|[^:/?#@\s[\]]+)(?::(\d{1,5}))?(\/[^?#\s]*)?$/;
|
|
3
|
+
/** Parses a DSN; a bad one comes back as a reason, never as an exception. */
|
|
4
|
+
export function parseDsn(input) {
|
|
5
|
+
if (typeof input !== "string" || input.trim() === "") {
|
|
6
|
+
return { error: "The DSN is empty." };
|
|
7
|
+
}
|
|
8
|
+
const text = input.trim();
|
|
9
|
+
if (!/^https?:\/\//.test(text)) {
|
|
10
|
+
return { error: "The DSN must start with https:// (or http://)." };
|
|
11
|
+
}
|
|
12
|
+
const m = DSN_RE.exec(text);
|
|
13
|
+
if (!m) {
|
|
14
|
+
return {
|
|
15
|
+
error: "The DSN should look like https://<key>@ingest.statoss.com/<app number>.",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const segments = (m[5] ?? "").split("/").filter(Boolean);
|
|
19
|
+
const projectId = segments.pop();
|
|
20
|
+
if (!projectId) {
|
|
21
|
+
return { error: "The DSN has no app number after the host." };
|
|
22
|
+
}
|
|
23
|
+
if (!/^\d+$/.test(projectId)) {
|
|
24
|
+
return {
|
|
25
|
+
error: `The DSN's app number must be digits, not "${projectId}".`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
dsn: {
|
|
30
|
+
protocol: m[1],
|
|
31
|
+
publicKey: m[2],
|
|
32
|
+
host: m[3],
|
|
33
|
+
port: m[4] ?? "",
|
|
34
|
+
path: segments.join("/"),
|
|
35
|
+
projectId,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function origin(dsn) {
|
|
40
|
+
return `${dsn.protocol}://${dsn.host}${dsn.port ? `:${dsn.port}` : ""}`;
|
|
41
|
+
}
|
|
42
|
+
/** The DSN without any secret part, as Sentry writes it in envelope headers. */
|
|
43
|
+
export function dsnToString(dsn) {
|
|
44
|
+
return `${dsn.protocol}://${dsn.publicKey}@${dsn.host}${dsn.port ? `:${dsn.port}` : ""}/${dsn.path ? `${dsn.path}/` : ""}${dsn.projectId}`;
|
|
45
|
+
}
|
|
46
|
+
export function envelopeEndpoint(dsn) {
|
|
47
|
+
return `${origin(dsn)}${dsn.path ? `/${dsn.path}` : ""}/api/${dsn.projectId}/envelope/`;
|
|
48
|
+
}
|
|
49
|
+
/** The endpoint with the key in the query, so browsers send without a CORS preflight. */
|
|
50
|
+
export function envelopeUrlWithQueryAuth(dsn) {
|
|
51
|
+
return `${envelopeEndpoint(dsn)}?sentry_version=7&sentry_key=${encodeURIComponent(dsn.publicKey)}&sentry_client=${encodeURIComponent(`${SDK_NAME}/${SDK_VERSION}`)}`;
|
|
52
|
+
}
|
|
53
|
+
export function authHeader(dsn) {
|
|
54
|
+
return `Sentry sentry_version=7, sentry_key=${dsn.publicKey}, sentry_client=${SDK_NAME}/${SDK_VERSION}`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Dsn } from "./dsn.js";
|
|
2
|
+
import type { Event } from "./types.js";
|
|
3
|
+
export interface DiscardedEvent {
|
|
4
|
+
reason: string;
|
|
5
|
+
category: string;
|
|
6
|
+
quantity: number;
|
|
7
|
+
}
|
|
8
|
+
export interface EnvelopeHeader {
|
|
9
|
+
event_id?: string;
|
|
10
|
+
sent_at: string;
|
|
11
|
+
dsn: string;
|
|
12
|
+
sdk: {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export type EventItem = [{
|
|
18
|
+
type: "event";
|
|
19
|
+
}, Event];
|
|
20
|
+
export type ClientReportItem = [
|
|
21
|
+
{
|
|
22
|
+
type: "client_report";
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
timestamp: number;
|
|
26
|
+
discarded_events: DiscardedEvent[];
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
export type EnvelopeItem = EventItem | ClientReportItem;
|
|
30
|
+
export type Envelope = [EnvelopeHeader, EnvelopeItem[]];
|
|
31
|
+
export declare function eventEnvelope(event: Event, dsn: Dsn): Envelope;
|
|
32
|
+
export declare function clientReportEnvelope(discarded: DiscardedEvent[], dsn: Dsn, timestamp?: number): Envelope;
|
|
33
|
+
/** Sentry's rate-limit category for an envelope item. */
|
|
34
|
+
export declare function itemCategory(item: EnvelopeItem): string;
|
|
35
|
+
/** Byte for byte what Sentry's serializeEnvelope produces for string payloads. */
|
|
36
|
+
export declare function serializeEnvelope(envelope: Envelope): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { dsnToString } from "./dsn.js";
|
|
2
|
+
import { normalize } from "./utils.js";
|
|
3
|
+
import { SDK_NAME, SDK_VERSION } from "./version.js";
|
|
4
|
+
function sdkHeader() {
|
|
5
|
+
return { name: SDK_NAME, version: SDK_VERSION };
|
|
6
|
+
}
|
|
7
|
+
export function eventEnvelope(event, dsn) {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
event_id: event.event_id,
|
|
11
|
+
sent_at: new Date().toISOString(),
|
|
12
|
+
dsn: dsnToString(dsn),
|
|
13
|
+
sdk: sdkHeader(),
|
|
14
|
+
},
|
|
15
|
+
[[{ type: "event" }, event]],
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
export function clientReportEnvelope(discarded, dsn, timestamp = Date.now() / 1000) {
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
sent_at: new Date().toISOString(),
|
|
22
|
+
dsn: dsnToString(dsn),
|
|
23
|
+
sdk: sdkHeader(),
|
|
24
|
+
},
|
|
25
|
+
[[{ type: "client_report" }, { timestamp, discarded_events: discarded }]],
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/** Sentry's rate-limit category for an envelope item. */
|
|
29
|
+
export function itemCategory(item) {
|
|
30
|
+
return item[0].type === "event" ? "error" : "internal";
|
|
31
|
+
}
|
|
32
|
+
function stringify(payload) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(payload);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// beforeSend may have put something JSON cannot hold back in.
|
|
38
|
+
return JSON.stringify(normalize(payload, 10));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Byte for byte what Sentry's serializeEnvelope produces for string payloads. */
|
|
42
|
+
export function serializeEnvelope(envelope) {
|
|
43
|
+
const [header, items] = envelope;
|
|
44
|
+
let out = JSON.stringify(header);
|
|
45
|
+
for (const [itemHeader, payload] of items) {
|
|
46
|
+
out += `\n${JSON.stringify(itemHeader)}\n${stringify(payload)}`;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Event, Mechanism, StackFrame } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Builds an event from anything thrown or rejected, as Sentry's SDKs do:
|
|
4
|
+
* errors with their stacks and causes, other objects and plain values as
|
|
5
|
+
* synthetic exceptions.
|
|
6
|
+
*/
|
|
7
|
+
export declare function eventFromUnknown(input: unknown, mechanism: Mechanism, rejection?: boolean): Event;
|
|
8
|
+
/** For a browser error event that carried no Error: the file and line it names. */
|
|
9
|
+
export declare function addInitialFrame(event: Event, frame: StackFrame): Event;
|