@flareapp/core 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/index.cjs +1152 -0
- package/dist/index.d.cts +544 -0
- package/dist/index.d.mts +544 -0
- package/dist/index.mjs +1104 -0
- package/package.json +4 -1
- package/.oxlintrc.json +0 -7
- package/.release-it.json +0 -13
- package/CHANGELOG.md +0 -16
- package/src/Flare.ts +0 -543
- package/src/Scope.ts +0 -96
- package/src/api/Api.ts +0 -35
- package/src/api/index.ts +0 -1
- package/src/env/index.ts +0 -14
- package/src/index.ts +0 -41
- package/src/stacktrace/NullFileReader.ts +0 -28
- package/src/stacktrace/createStackTrace.ts +0 -74
- package/src/stacktrace/fileReader.ts +0 -96
- package/src/stacktrace/index.ts +0 -4
- package/src/types.ts +0 -81
- package/src/util/assert.ts +0 -9
- package/src/util/assertKey.ts +0 -11
- package/src/util/convertToError.ts +0 -22
- package/src/util/extractCode.ts +0 -11
- package/src/util/flatJsonStringify.ts +0 -45
- package/src/util/glowsToEvents.ts +0 -16
- package/src/util/index.ts +0 -8
- package/src/util/now.ts +0 -3
- package/src/util/redactUrl.ts +0 -83
- package/tests/api.test.ts +0 -95
- package/tests/configure.test.ts +0 -16
- package/tests/contextCollector.test.ts +0 -37
- package/tests/convertToError.test.ts +0 -95
- package/tests/createStackTrace.test.ts +0 -54
- package/tests/extractCode.test.ts +0 -30
- package/tests/fileReader.test.ts +0 -51
- package/tests/flatJsonStringify.test.ts +0 -31
- package/tests/flush.test.ts +0 -47
- package/tests/glows.test.ts +0 -47
- package/tests/glowsToEvents.test.ts +0 -41
- package/tests/helpers/FakeApi.ts +0 -20
- package/tests/helpers/index.ts +0 -1
- package/tests/hooks.test.ts +0 -123
- package/tests/light.test.ts +0 -25
- package/tests/nullFileReader.test.ts +0 -11
- package/tests/publicExports.test.ts +0 -17
- package/tests/redactUrl.test.ts +0 -151
- package/tests/report.test.ts +0 -146
- package/tests/sampleRate.test.ts +0 -88
- package/tests/scope.test.ts +0 -64
- package/tests/setEntryPoint.test.ts +0 -79
- package/tests/setFramework.test.ts +0 -48
- package/tests/setSdkInfo.test.ts +0 -62
- package/tsconfig.json +0 -4
- package/vitest.config.ts +0 -17
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type MessageLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';
|
|
3
|
+
type AttributeValue = string | number | boolean | null | AttributeValue[] | {
|
|
4
|
+
[key: string]: AttributeValue;
|
|
5
|
+
};
|
|
6
|
+
type Attributes = Record<string, AttributeValue>;
|
|
7
|
+
type Config = {
|
|
8
|
+
key: string | null;
|
|
9
|
+
version: string;
|
|
10
|
+
sourcemapVersionId: string;
|
|
11
|
+
stage: string;
|
|
12
|
+
maxGlowsPerReport: number;
|
|
13
|
+
reportBrowserExtensionErrors: boolean;
|
|
14
|
+
ingestUrl: string;
|
|
15
|
+
debug: boolean;
|
|
16
|
+
urlDenylist: RegExp;
|
|
17
|
+
replaceDefaultUrlDenylist: boolean;
|
|
18
|
+
sampleRate: number;
|
|
19
|
+
enableLogs: boolean;
|
|
20
|
+
logsIngestUrl: string;
|
|
21
|
+
minimumLogLevel?: MessageLevel;
|
|
22
|
+
serviceName?: string;
|
|
23
|
+
maxLogBufferSize: number;
|
|
24
|
+
logFlushIntervalMs: number;
|
|
25
|
+
logFlushMaxBytes: number;
|
|
26
|
+
keepaliveMaxBytes: number;
|
|
27
|
+
beforeEvaluate: (error: Error) => Error | false | null | Promise<Error | false | null>;
|
|
28
|
+
beforeSubmit: (report: Report) => Report | false | null | Promise<Report | false | null>;
|
|
29
|
+
};
|
|
30
|
+
type StackFrame = {
|
|
31
|
+
file: string;
|
|
32
|
+
lineNumber: number;
|
|
33
|
+
columnNumber?: number;
|
|
34
|
+
method?: string;
|
|
35
|
+
class?: string;
|
|
36
|
+
codeSnippet?: {
|
|
37
|
+
[line: number]: string;
|
|
38
|
+
};
|
|
39
|
+
isApplicationFrame?: boolean;
|
|
40
|
+
arguments?: unknown[];
|
|
41
|
+
};
|
|
42
|
+
type SpanEvent = {
|
|
43
|
+
type: string;
|
|
44
|
+
startTimeUnixNano: number;
|
|
45
|
+
endTimeUnixNano: number | null;
|
|
46
|
+
attributes: Attributes;
|
|
47
|
+
};
|
|
48
|
+
type OverriddenGrouping = 'exception_class' | 'exception_message' | 'exception_message_and_class' | 'full_stacktrace_and_exception_class_and_code';
|
|
49
|
+
type Report = {
|
|
50
|
+
exceptionClass?: string | null;
|
|
51
|
+
message?: string | null;
|
|
52
|
+
code?: string;
|
|
53
|
+
seenAtUnixNano: number;
|
|
54
|
+
isLog?: boolean;
|
|
55
|
+
level?: MessageLevel;
|
|
56
|
+
sourcemapVersionId?: string;
|
|
57
|
+
trackingUuid?: string;
|
|
58
|
+
handled?: boolean;
|
|
59
|
+
openFrameIndex?: number;
|
|
60
|
+
applicationPath?: string;
|
|
61
|
+
overriddenGrouping?: OverriddenGrouping | null;
|
|
62
|
+
stacktrace: StackFrame[];
|
|
63
|
+
events: SpanEvent[];
|
|
64
|
+
attributes: Attributes;
|
|
65
|
+
};
|
|
66
|
+
type Glow = {
|
|
67
|
+
time: number;
|
|
68
|
+
microtime: number;
|
|
69
|
+
name: string;
|
|
70
|
+
messageLevel: MessageLevel;
|
|
71
|
+
metaData: Record<string, unknown> | Record<string, unknown>[];
|
|
72
|
+
};
|
|
73
|
+
type EntryPointHandler = {
|
|
74
|
+
identifier?: string;
|
|
75
|
+
name?: string;
|
|
76
|
+
type?: string;
|
|
77
|
+
};
|
|
78
|
+
type SdkInfo = {
|
|
79
|
+
name: string;
|
|
80
|
+
version: string;
|
|
81
|
+
};
|
|
82
|
+
type Framework = {
|
|
83
|
+
name: string;
|
|
84
|
+
version?: string;
|
|
85
|
+
};
|
|
86
|
+
type AnyValue = {
|
|
87
|
+
stringValue: string;
|
|
88
|
+
} | {
|
|
89
|
+
boolValue: boolean;
|
|
90
|
+
} | {
|
|
91
|
+
intValue: number;
|
|
92
|
+
} | {
|
|
93
|
+
doubleValue: number;
|
|
94
|
+
} | {
|
|
95
|
+
arrayValue: {
|
|
96
|
+
values: AnyValue[];
|
|
97
|
+
};
|
|
98
|
+
} | {
|
|
99
|
+
kvlistValue: {
|
|
100
|
+
values: KeyValue[];
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
type KeyValue = {
|
|
104
|
+
key: string;
|
|
105
|
+
value: AnyValue;
|
|
106
|
+
};
|
|
107
|
+
type OtelResource = {
|
|
108
|
+
attributes: KeyValue[];
|
|
109
|
+
droppedAttributesCount: number;
|
|
110
|
+
};
|
|
111
|
+
type OtelScope = {
|
|
112
|
+
name: string;
|
|
113
|
+
version: string;
|
|
114
|
+
attributes: KeyValue[];
|
|
115
|
+
droppedAttributesCount: number;
|
|
116
|
+
};
|
|
117
|
+
type OtelLogRecord = {
|
|
118
|
+
timeUnixNano: string;
|
|
119
|
+
observedTimeUnixNano: string;
|
|
120
|
+
severityNumber: number;
|
|
121
|
+
severityText: string;
|
|
122
|
+
body: AnyValue;
|
|
123
|
+
attributes: KeyValue[];
|
|
124
|
+
flags: number;
|
|
125
|
+
droppedAttributesCount: number;
|
|
126
|
+
};
|
|
127
|
+
type LogsEnvelope = {
|
|
128
|
+
resourceLogs: Array<{
|
|
129
|
+
resource: OtelResource;
|
|
130
|
+
scopeLogs: Array<{
|
|
131
|
+
scope: OtelScope;
|
|
132
|
+
logRecords: OtelLogRecord[];
|
|
133
|
+
}>;
|
|
134
|
+
}>;
|
|
135
|
+
};
|
|
136
|
+
type BufferedLog = {
|
|
137
|
+
timeUnixNano: string;
|
|
138
|
+
severityNumber: number;
|
|
139
|
+
severityText: string;
|
|
140
|
+
message: string;
|
|
141
|
+
recordAttributes: KeyValue[];
|
|
142
|
+
resourceAttributes: Attributes;
|
|
143
|
+
};
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/util/assert.d.ts
|
|
146
|
+
declare function assert(value: unknown, message: string, debug: boolean): boolean;
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/util/assertKey.d.ts
|
|
149
|
+
declare function assertKey(key: unknown, debug: boolean): boolean;
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/util/convertToError.d.ts
|
|
152
|
+
declare function convertToError(error: unknown): Error;
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/util/extractCode.d.ts
|
|
155
|
+
declare function extractCode(error: Error): string | undefined;
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/util/flatJsonStringify.d.ts
|
|
158
|
+
declare function flatJsonStringify(json: object): string;
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/util/glowsToEvents.d.ts
|
|
161
|
+
declare function glowsToEvents(glows: Glow[]): SpanEvent[];
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/util/now.d.ts
|
|
164
|
+
declare function now(): number;
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/util/redactUrl.d.ts
|
|
167
|
+
declare const DEFAULT_URL_DENYLIST: RegExp;
|
|
168
|
+
declare function resolveDenylist(custom?: RegExp, replaceDefault?: boolean, defaultDenylist?: RegExp): RegExp;
|
|
169
|
+
declare function redactUrlQuery(fullPath: string, denylist?: RegExp): string;
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/api/Api.d.ts
|
|
172
|
+
declare class Api {
|
|
173
|
+
report(report: Report, url: string, key: string | null, reportBrowserExtensionErrors: boolean, debug?: boolean): Promise<void>;
|
|
174
|
+
logs(envelope: LogsEnvelope, url: string, key: string | null, debug?: boolean, keepalive?: boolean): Promise<void>;
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/logging/FlushScheduler.d.ts
|
|
178
|
+
type FlushFn = (opts?: {
|
|
179
|
+
keepalive?: boolean;
|
|
180
|
+
}) => void;
|
|
181
|
+
/**
|
|
182
|
+
* The seam through which a platform package wires the "drain on lifecycle end"
|
|
183
|
+
* trigger (browser unload, Node process exit). Core ships a no-op default; the
|
|
184
|
+
* count/weight/timer batching policy lives in `Logger` regardless.
|
|
185
|
+
*/
|
|
186
|
+
interface FlushScheduler {
|
|
187
|
+
register(flush: FlushFn): void;
|
|
188
|
+
}
|
|
189
|
+
declare class NoopFlushScheduler implements FlushScheduler {
|
|
190
|
+
register(): void;
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/logging/Logger.d.ts
|
|
194
|
+
type LoggerDeps = {
|
|
195
|
+
api: Api;
|
|
196
|
+
getConfig: () => Config;
|
|
197
|
+
getSdkInfo: () => SdkInfo;
|
|
198
|
+
getFramework: () => Framework | null;
|
|
199
|
+
buildLogAttributes: (userAttributes: Attributes) => {
|
|
200
|
+
record: Attributes;
|
|
201
|
+
resource: Attributes;
|
|
202
|
+
};
|
|
203
|
+
track: <T>(p: Promise<T>) => Promise<T>;
|
|
204
|
+
scheduler: FlushScheduler;
|
|
205
|
+
};
|
|
206
|
+
declare class Logger {
|
|
207
|
+
private deps;
|
|
208
|
+
private buffer;
|
|
209
|
+
private resourceAttributes;
|
|
210
|
+
private timer;
|
|
211
|
+
private timerActive;
|
|
212
|
+
constructor(deps: LoggerDeps);
|
|
213
|
+
debug(message: string, context?: Attributes, attributes?: Attributes): void;
|
|
214
|
+
info(message: string, context?: Attributes, attributes?: Attributes): void;
|
|
215
|
+
notice(message: string, context?: Attributes, attributes?: Attributes): void;
|
|
216
|
+
warning(message: string, context?: Attributes, attributes?: Attributes): void;
|
|
217
|
+
error(message: string, context?: Attributes, attributes?: Attributes): void;
|
|
218
|
+
critical(message: string, context?: Attributes, attributes?: Attributes): void;
|
|
219
|
+
alert(message: string, context?: Attributes, attributes?: Attributes): void;
|
|
220
|
+
emergency(message: string, context?: Attributes, attributes?: Attributes): void;
|
|
221
|
+
bufferLength(): number;
|
|
222
|
+
private record;
|
|
223
|
+
private evaluateTriggers;
|
|
224
|
+
private armTimer;
|
|
225
|
+
private trim;
|
|
226
|
+
flush(opts?: {
|
|
227
|
+
keepalive?: boolean;
|
|
228
|
+
}): void;
|
|
229
|
+
clear(): void;
|
|
230
|
+
private packForKeepalive;
|
|
231
|
+
private buildEnvelope;
|
|
232
|
+
private resourceForFlush;
|
|
233
|
+
private clearTimer;
|
|
234
|
+
private estimateBytes;
|
|
235
|
+
private bufferBytes;
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/Scope.d.ts
|
|
239
|
+
/**
|
|
240
|
+
* Holds the per-call mutable state that used to live on the `Flare` instance:
|
|
241
|
+
* breadcrumbs (`glows`), custom attributes (`pendingAttributes`), and the
|
|
242
|
+
* current entry-point handler.
|
|
243
|
+
*
|
|
244
|
+
* Why this exists as its own class: in the browser there is one `Flare` per
|
|
245
|
+
* page and one user at a time, so a single shared bag of state is fine. In
|
|
246
|
+
* Node, a single `Flare` instance serves many concurrent requests, and each
|
|
247
|
+
* request wants its own breadcrumbs and its own custom context that do NOT
|
|
248
|
+
* leak into other requests. Splitting this state out of `Flare` lets the
|
|
249
|
+
* consumer choose: one global `Scope` (browser) or one `Scope` per request
|
|
250
|
+
* via AsyncLocalStorage (Node).
|
|
251
|
+
*
|
|
252
|
+
* `Flare` reads and writes this through `scopeProvider.active()` instead of
|
|
253
|
+
* holding the state directly, so the per-request behavior comes from the
|
|
254
|
+
* provider, not from the class itself.
|
|
255
|
+
*
|
|
256
|
+
* `NodeScope` (in `@flareapp/node`) extends this with two more buckets:
|
|
257
|
+
* `request` (HTTP method, path, headers) and `user` (id, email, ...). Browser
|
|
258
|
+
* does not need those.
|
|
259
|
+
*/
|
|
260
|
+
declare class Scope {
|
|
261
|
+
glows: Glow[];
|
|
262
|
+
pendingAttributes: Attributes;
|
|
263
|
+
entryPoint: EntryPointHandler | null;
|
|
264
|
+
/**
|
|
265
|
+
* Append a breadcrumb. Caps the list at `maxGlowsPerReport` by dropping the
|
|
266
|
+
* OLDEST entries when the limit is exceeded; this keeps reports below a
|
|
267
|
+
* payload-size threshold while preserving the most recent events leading
|
|
268
|
+
* up to an error.
|
|
269
|
+
*
|
|
270
|
+
* `slice(length - max)` returns the trailing `max` items, which is the
|
|
271
|
+
* shortest way to drop from the front and keep insertion order.
|
|
272
|
+
*/
|
|
273
|
+
addGlow(glow: Glow, maxGlowsPerReport: number): void;
|
|
274
|
+
clearGlows(): void;
|
|
275
|
+
/**
|
|
276
|
+
* Set a single attribute on this scope. Called from `Flare.addContext` and
|
|
277
|
+
* `Flare.addContextGroup`. Last write wins.
|
|
278
|
+
*/
|
|
279
|
+
setAttribute(key: string, value: AttributeValue): void;
|
|
280
|
+
/**
|
|
281
|
+
* Shallow-merge a bag of attributes into this scope. Used by Node's
|
|
282
|
+
* AsyncLocalStorage provider when patching the live request context via
|
|
283
|
+
* `flare.mergeContext({ ... })`. Last write wins per key; nested objects
|
|
284
|
+
* are NOT deep-merged.
|
|
285
|
+
*/
|
|
286
|
+
mergeAttributes(partial: Attributes): void;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The seam through which `Flare` reaches its current `Scope`. Implementations
|
|
290
|
+
* decide what "current" means.
|
|
291
|
+
*
|
|
292
|
+
* - `GlobalScopeProvider` always returns the same `Scope` instance (browser).
|
|
293
|
+
* - `AsyncLocalStorageScopeProvider` in `@flareapp/node` returns the per-request
|
|
294
|
+
* `NodeScope` stored in `node:async_hooks` for the in-flight async chain,
|
|
295
|
+
* falling back to a single shared scope when called outside any
|
|
296
|
+
* `runWithContext(...)` callback.
|
|
297
|
+
*
|
|
298
|
+
* Any consumer of `@flareapp/core` can supply its own provider to plug in
|
|
299
|
+
* different "current scope" semantics.
|
|
300
|
+
*/
|
|
301
|
+
interface ScopeProvider {
|
|
302
|
+
active(): Scope;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The simplest provider: one `Scope` for the lifetime of the provider, shared
|
|
306
|
+
* by every caller. This is the right default for environments with a single
|
|
307
|
+
* logical context (browser tab, CLI script, etc.) and is the default that
|
|
308
|
+
* `Flare`'s constructor falls back to when no provider is supplied.
|
|
309
|
+
*/
|
|
310
|
+
declare class GlobalScopeProvider implements ScopeProvider {
|
|
311
|
+
private scope;
|
|
312
|
+
active(): Scope;
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/stacktrace/fileReader.d.ts
|
|
316
|
+
interface FileReader {
|
|
317
|
+
read(url: string): Promise<string | null>;
|
|
318
|
+
}
|
|
319
|
+
type CodeSnippet = {
|
|
320
|
+
[key: number]: string;
|
|
321
|
+
};
|
|
322
|
+
type ReaderResponse = {
|
|
323
|
+
codeSnippet: CodeSnippet;
|
|
324
|
+
trimmedColumnNumber: number | null;
|
|
325
|
+
};
|
|
326
|
+
declare function getCodeSnippet(fileReader: FileReader, url?: string, lineNumber?: number, columnNumber?: number): Promise<ReaderResponse>;
|
|
327
|
+
declare function readLinesFromFile(fileText: string, lineNumber: number, columnNumber?: number, maxSnippetLineLength?: number, maxSnippetLines?: number): ReaderResponse;
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/Flare.d.ts
|
|
330
|
+
type ContextCollector = (config: Readonly<Config>) => Attributes;
|
|
331
|
+
declare class Flare {
|
|
332
|
+
api: Api;
|
|
333
|
+
private contextCollector;
|
|
334
|
+
private fileReader;
|
|
335
|
+
private scopeProvider;
|
|
336
|
+
private inflight;
|
|
337
|
+
private _logger;
|
|
338
|
+
private _config;
|
|
339
|
+
private sdkInfo;
|
|
340
|
+
private framework;
|
|
341
|
+
/**
|
|
342
|
+
* @param api sends the report over HTTP.
|
|
343
|
+
* @param contextCollector returns per-report attributes (browser DOM info, Node
|
|
344
|
+
* process info, etc). Default is a no-op.
|
|
345
|
+
* @param fileReader reads source files for stack-trace snippets. Default
|
|
346
|
+
* returns null (no snippets); `@flareapp/js` injects a
|
|
347
|
+
* fetch-based reader, `@flareapp/node` injects a disk reader.
|
|
348
|
+
* @param scopeProvider returns the current `Scope` (per-call mutable state:
|
|
349
|
+
* glows, pendingAttributes, entryPoint). Browser uses a
|
|
350
|
+
* single global scope; Node uses an AsyncLocalStorage-
|
|
351
|
+
* backed provider so each request gets its own.
|
|
352
|
+
*/
|
|
353
|
+
constructor(api?: Api, contextCollector?: ContextCollector, fileReader?: FileReader, scopeProvider?: ScopeProvider, scheduler?: FlushScheduler);
|
|
354
|
+
/**
|
|
355
|
+
* Register an in-flight report so `flush()` can wait for it. Called by
|
|
356
|
+
* every public report entry point (`report`, `reportSilently`,
|
|
357
|
+
* `reportMessage`, `reportUnhandledRejection`, `test`); each wraps its
|
|
358
|
+
* full async pipeline (beforeEvaluate -> stack trace + source snippets ->
|
|
359
|
+
* beforeSubmit -> `api.report()`) so the entire roundtrip is what's
|
|
360
|
+
* tracked, not just the HTTP send at the end.
|
|
361
|
+
*
|
|
362
|
+
* Two problems this method solves at once.
|
|
363
|
+
*
|
|
364
|
+
* Problem 1: hold a reference to the work without leaking rejections.
|
|
365
|
+
*
|
|
366
|
+
* `p` is the real report pipeline; it can reject (network failure,
|
|
367
|
+
* `beforeSubmit` throws, etc). If we stored `p` directly in `inflight`
|
|
368
|
+
* and no caller attached a `.catch` (the global error listeners use
|
|
369
|
+
* `reportSilently` which DOES catch, but the path is still subtle), an
|
|
370
|
+
* eventual rejection would surface as an unhandled-rejection warning
|
|
371
|
+
* on Node and a console error in the browser. Bad citizen.
|
|
372
|
+
*
|
|
373
|
+
* So we build a SHADOW promise that mirrors `p`'s timing but cannot
|
|
374
|
+
* reject:
|
|
375
|
+
*
|
|
376
|
+
* p.then(
|
|
377
|
+
* () => undefined, // on fulfilment, value is undefined
|
|
378
|
+
* () => undefined, // on rejection, ALSO resolve with undefined
|
|
379
|
+
* )
|
|
380
|
+
*
|
|
381
|
+
* Providing the second argument means we have "handled" any rejection
|
|
382
|
+
* from `p`. The shadow always resolves with `undefined`, and `p`'s
|
|
383
|
+
* rejection is consumed at the boundary. From the runtime's point of
|
|
384
|
+
* view, the shadow is well-behaved.
|
|
385
|
+
*
|
|
386
|
+
* Problem 2: self-cleaning entry.
|
|
387
|
+
*
|
|
388
|
+
* `tracked.finally(() => this.inflight.delete(tracked))`. `finally`
|
|
389
|
+
* fires whether the shadow resolves or rejects, but the shadow can no
|
|
390
|
+
* longer reject (problem 1 normalized it), so this is effectively
|
|
391
|
+
* "when the underlying report has settled, remove me from the Set."
|
|
392
|
+
* No GC magic, no external cleanup, no race window.
|
|
393
|
+
*
|
|
394
|
+
* Note that `.finally` itself returns a new promise that we drop on
|
|
395
|
+
* the floor. If the cleanup callback ever throws, that would surface
|
|
396
|
+
* as an unhandled rejection on the dropped promise; `delete` does not
|
|
397
|
+
* throw so we are safe today, but anything more elaborate added here
|
|
398
|
+
* should be wrapped in try/catch.
|
|
399
|
+
*
|
|
400
|
+
* The return value is the ORIGINAL `p`. The caller awaits real success
|
|
401
|
+
* or failure; the tracking is completely invisible to them. This is why
|
|
402
|
+
* `await flare.report(err)` inside a fatal handler observes network
|
|
403
|
+
* errors the same as before tracking was added.
|
|
404
|
+
*/
|
|
405
|
+
private track;
|
|
406
|
+
/**
|
|
407
|
+
* Wait until every in-flight report settles, or until `timeoutMs`
|
|
408
|
+
* elapses, whichever comes first. Always resolves; never rejects.
|
|
409
|
+
*
|
|
410
|
+
* The main consumer is `@flareapp/node`'s fatal handler:
|
|
411
|
+
*
|
|
412
|
+
* process.on('uncaughtException', async (err) => {
|
|
413
|
+
* process.exitCode = 1;
|
|
414
|
+
* try { await flare.report(err); } catch {}
|
|
415
|
+
* await flare.flush(shutdownTimeoutMs);
|
|
416
|
+
* process.exit(1);
|
|
417
|
+
* });
|
|
418
|
+
*
|
|
419
|
+
* The fatal `report` is awaited explicitly; `flush` then drains any
|
|
420
|
+
* OTHER reports that were already in flight (a request handler that
|
|
421
|
+
* fired `flare.report(...)` concurrently with the crash). The timeout
|
|
422
|
+
* caps the wait so a hung HTTP request cannot indefinitely block
|
|
423
|
+
* shutdown.
|
|
424
|
+
*
|
|
425
|
+
* Walking the implementation:
|
|
426
|
+
*
|
|
427
|
+
* const pending = [...this.inflight];
|
|
428
|
+
*
|
|
429
|
+
* Spread takes a SNAPSHOT of the Set at this instant. Reports that
|
|
430
|
+
* start AFTER this line are not included in `pending`, so they are
|
|
431
|
+
* not awaited by THIS flush call. This is intentional: it bounds
|
|
432
|
+
* the wait. Without the snapshot, a handler that kept emitting
|
|
433
|
+
* reports during shutdown could keep flush alive forever and block
|
|
434
|
+
* the process from exiting.
|
|
435
|
+
*
|
|
436
|
+
* if (pending.length === 0) return Promise.resolve();
|
|
437
|
+
*
|
|
438
|
+
* Fast path. No timer scheduled, no promise constructor needed.
|
|
439
|
+
* Resolves on the microtask queue. Cheap.
|
|
440
|
+
*
|
|
441
|
+
* return new Promise<void>((resolve) => {
|
|
442
|
+
* const timer = setTimeout(resolve, timeoutMs);
|
|
443
|
+
* Promise.allSettled(pending).then(() => {
|
|
444
|
+
* clearTimeout(timer);
|
|
445
|
+
* resolve();
|
|
446
|
+
* });
|
|
447
|
+
* });
|
|
448
|
+
*
|
|
449
|
+
* The race between two outcomes, both calling the same `resolve`:
|
|
450
|
+
*
|
|
451
|
+
* 1. `setTimeout(resolve, timeoutMs)` schedules a "give up" call.
|
|
452
|
+
* After `timeoutMs` it fires, calling `resolve()` from the
|
|
453
|
+
* timer-queue side. The outer promise resolves immediately,
|
|
454
|
+
* even if reports are still pending. Those reports are abandoned
|
|
455
|
+
* (they continue running but the process is about to die).
|
|
456
|
+
*
|
|
457
|
+
* 2. `Promise.allSettled(pending)` returns a promise that resolves
|
|
458
|
+
* when every promise in `pending` has either fulfilled or
|
|
459
|
+
* rejected. It NEVER rejects on its own. We use `allSettled`
|
|
460
|
+
* rather than `Promise.all` because `all` short-circuits on the
|
|
461
|
+
* first rejection -- we want to wait for everyone regardless of
|
|
462
|
+
* whether their HTTP calls succeed or fail. (Our shadows cannot
|
|
463
|
+
* reject anyway because `track` normalized them, but using
|
|
464
|
+
* `allSettled` documents the intent and survives future changes
|
|
465
|
+
* to shadow construction.) When it resolves, we call
|
|
466
|
+
* `clearTimeout(timer)` to cancel the pending timer (so it does
|
|
467
|
+
* not fire later and call `resolve` a second time -- a no-op,
|
|
468
|
+
* but wasted work) and then `resolve()` ourselves.
|
|
469
|
+
*
|
|
470
|
+
* Resolve can only meaningfully fire once. Subsequent calls to the
|
|
471
|
+
* same `resolve` are silently ignored by the Promise spec, so the
|
|
472
|
+
* race is safe even if for some reason both branches fired together.
|
|
473
|
+
*
|
|
474
|
+
* Things flush() deliberately does NOT do:
|
|
475
|
+
*
|
|
476
|
+
* - It does not reject. Even if every report failed, allSettled
|
|
477
|
+
* resolves. Callers do not need a `.catch`.
|
|
478
|
+
* - It does not retry. One pipeline attempt per report, then move on.
|
|
479
|
+
* - It does not stop new reports from starting. The Flare instance
|
|
480
|
+
* is still usable after flush resolves. flush is "wait for what is
|
|
481
|
+
* in flight," not "freeze the SDK."
|
|
482
|
+
* - It does not drain reports started after the snapshot. Call flush
|
|
483
|
+
* again if you need to wait for those too.
|
|
484
|
+
*/
|
|
485
|
+
flush(timeoutMs?: number): Promise<void>;
|
|
486
|
+
get config(): Readonly<Config>;
|
|
487
|
+
get glows(): readonly Glow[];
|
|
488
|
+
get logger(): Logger;
|
|
489
|
+
light(key?: string, debug?: boolean): this;
|
|
490
|
+
configure(config: Partial<Config>): this;
|
|
491
|
+
test(): Promise<void>;
|
|
492
|
+
private testInternal;
|
|
493
|
+
glow(name: string, level?: MessageLevel, data?: Record<string, unknown> | Record<string, unknown>[]): this;
|
|
494
|
+
clearGlows(): this;
|
|
495
|
+
addContext(name: string, value: AttributeValue): this;
|
|
496
|
+
addContextGroup(groupName: string, value: Record<string, AttributeValue>): this;
|
|
497
|
+
setEntryPoint(handler: EntryPointHandler): this;
|
|
498
|
+
setSdkInfo(info: SdkInfo): this;
|
|
499
|
+
setFramework(framework: Framework): this;
|
|
500
|
+
report(error: Error, attributes?: Attributes): Promise<void>;
|
|
501
|
+
private reportInternal;
|
|
502
|
+
reportSilently(error: Error, attributes?: Attributes): void;
|
|
503
|
+
reportUnhandledRejection(message: string, attributes?: Attributes): Promise<void>;
|
|
504
|
+
private reportUnhandledRejectionInternal;
|
|
505
|
+
reportMessage(message: string, level?: MessageLevel, attributes?: Attributes): Promise<void>;
|
|
506
|
+
private reportMessageInternal;
|
|
507
|
+
createReportFromError(error: Error, attributes?: Attributes, seenAtUnixNano?: number): Promise<Report | false>;
|
|
508
|
+
private buildBaseAttributes;
|
|
509
|
+
private assembleAttributes;
|
|
510
|
+
private buildLogAttributes;
|
|
511
|
+
private buildReport;
|
|
512
|
+
sendReport(report: Report): Promise<void>;
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/stacktrace/NullFileReader.d.ts
|
|
516
|
+
/**
|
|
517
|
+
* No-op `FileReader` that returns `null` for every URL it is asked to read.
|
|
518
|
+
*
|
|
519
|
+
* Used as the default for `Flare`'s `fileReader` constructor parameter so the
|
|
520
|
+
* class is usable without picking a side: instantiated bare (`new Flare()`),
|
|
521
|
+
* reports still build, but stack frames omit source-code snippets — which is
|
|
522
|
+
* the correct, safe behavior in an environment we know nothing about.
|
|
523
|
+
*
|
|
524
|
+
* The two real implementations live in the consumer packages and take their
|
|
525
|
+
* place once the right environment is established:
|
|
526
|
+
*
|
|
527
|
+
* - `@flareapp/js` injects `FetchFileReader`, which `fetch()`s source maps
|
|
528
|
+
* and original files over HTTP for browser stack frames.
|
|
529
|
+
* - `@flareapp/node` injects `DiskFileReader`, which reads files from disk
|
|
530
|
+
* via `node:fs/promises` for server stack frames.
|
|
531
|
+
*
|
|
532
|
+
* The interface (`read(url) -> Promise<string | null>`) lets the stack-trace
|
|
533
|
+
* builder treat all three the same way: ask for a URL, render the snippet
|
|
534
|
+
* when text comes back, gracefully skip it when `null` does. No environment
|
|
535
|
+
* checks anywhere in core.
|
|
536
|
+
*/
|
|
537
|
+
declare class NullFileReader implements FileReader {
|
|
538
|
+
read(_url: string): Promise<string | null>;
|
|
539
|
+
}
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/stacktrace/createStackTrace.d.ts
|
|
542
|
+
declare function createStackTrace(error: Error, debug: boolean, fileReader: FileReader): Promise<Array<StackFrame>>;
|
|
543
|
+
//#endregion
|
|
544
|
+
export { type AnyValue, Api, type AttributeValue, type Attributes, type BufferedLog, type Config, type ContextCollector, DEFAULT_URL_DENYLIST, type EntryPointHandler, type FileReader, Flare, type FlushFn, type FlushScheduler, type Framework, GlobalScopeProvider, type Glow, type KeyValue, Logger, type LoggerDeps, type LogsEnvelope, type MessageLevel, NoopFlushScheduler, NullFileReader, type OtelLogRecord, type OverriddenGrouping, type Report, Scope, type ScopeProvider, type SdkInfo, type SpanEvent, type StackFrame, assert, assertKey, convertToError, createStackTrace, extractCode, flatJsonStringify, getCodeSnippet, glowsToEvents, now, readLinesFromFile, redactUrlQuery, resolveDenylist };
|