@duckbug/js 0.1.2 → 1.0.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 +348 -73
- package/dist/cjs/DuckBug/DuckBugHelper.cjs +136 -0
- package/dist/cjs/DuckBug/DuckBugProvider.cjs +82 -13
- package/dist/cjs/DuckBug/DuckBugService.cjs +158 -17
- package/dist/cjs/DuckBug/Pond.cjs +44 -0
- package/dist/cjs/DuckBug/ensureEventId.cjs +47 -0
- package/dist/cjs/DuckBug/finalizeIngestEvent.cjs +54 -0
- package/dist/cjs/DuckBug/index.cjs +18 -2
- package/dist/cjs/DuckBug/parseDuckBugDsn.cjs +82 -0
- package/dist/cjs/DuckBug/sanitizeIngestPayload.cjs +67 -0
- package/dist/cjs/DuckBug/stripIngestSections.cjs +43 -0
- package/dist/cjs/DuckBug/transportTypes.cjs +18 -0
- package/dist/cjs/SDK/DuckSDK.cjs +131 -12
- package/dist/cjs/SDK/index.cjs +5 -2
- package/dist/cjs/contract/index.cjs +18 -0
- package/dist/cjs/contract/ingestEvents.cjs +18 -0
- package/dist/cjs/index.cjs +24 -6
- package/dist/cjs/integrations/node.cjs +54 -0
- package/dist/cjs/sdkIdentity.cjs +47 -0
- package/dist/esm/DuckBug/DuckBugHelper.js +99 -0
- package/dist/esm/DuckBug/DuckBugProvider.js +82 -13
- package/dist/esm/DuckBug/DuckBugService.js +156 -15
- package/dist/esm/DuckBug/Pond.js +10 -0
- package/dist/esm/DuckBug/ensureEventId.js +13 -0
- package/dist/esm/DuckBug/finalizeIngestEvent.js +20 -0
- package/dist/esm/DuckBug/index.js +5 -1
- package/dist/esm/DuckBug/parseDuckBugDsn.js +36 -0
- package/dist/esm/DuckBug/sanitizeIngestPayload.js +33 -0
- package/dist/esm/DuckBug/stripIngestSections.js +9 -0
- package/dist/esm/DuckBug/transportTypes.js +0 -0
- package/dist/esm/SDK/DuckSDK.js +127 -11
- package/dist/esm/SDK/index.js +2 -2
- package/dist/esm/contract/index.js +0 -0
- package/dist/esm/contract/ingestEvents.js +0 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/integrations/node.js +20 -0
- package/dist/esm/sdkIdentity.js +7 -0
- package/dist/types/DuckBug/DuckBugConfig.d.ts +28 -0
- package/dist/types/DuckBug/DuckBugHelper.d.ts +22 -0
- package/dist/types/DuckBug/DuckBugProvider.d.ts +12 -1
- package/dist/types/DuckBug/DuckBugService.d.ts +30 -8
- package/dist/types/DuckBug/Log.d.ts +1 -7
- package/dist/types/DuckBug/Pond.d.ts +9 -0
- package/dist/types/DuckBug/ensureEventId.d.ts +4 -0
- package/dist/types/DuckBug/finalizeIngestEvent.d.ts +11 -0
- package/dist/types/DuckBug/index.d.ts +7 -1
- package/dist/types/DuckBug/parseDuckBugDsn.d.ts +17 -0
- package/dist/types/DuckBug/sanitizeIngestPayload.d.ts +4 -0
- package/dist/types/DuckBug/stripIngestSections.d.ts +4 -0
- package/dist/types/DuckBug/transportTypes.d.ts +7 -0
- package/dist/types/SDK/DuckSDK.d.ts +31 -4
- package/dist/types/SDK/Provider.d.ts +11 -3
- package/dist/types/SDK/index.d.ts +3 -2
- package/dist/types/contract/index.d.ts +1 -0
- package/dist/types/contract/ingestEvents.d.ts +58 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/integrations/node.d.ts +12 -0
- package/dist/types/sdkIdentity.d.ts +7 -0
- package/package.json +23 -7
- package/schemas/error-event.schema.json +147 -0
- package/schemas/log-event.schema.json +126 -0
|
@@ -1,24 +1,165 @@
|
|
|
1
|
+
import { ingestErrorsBatchUrl, ingestErrorsUrl, ingestLogsBatchUrl, ingestLogsUrl, parseDuckBugIngestDsn } from "./parseDuckBugDsn.js";
|
|
2
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
3
|
+
const DEFAULT_RETRY_DELAY_MS = 200;
|
|
4
|
+
const MAX_BACKEND_BATCH = 1000;
|
|
5
|
+
function isRetriableHttpStatus(status) {
|
|
6
|
+
return 408 === status || 429 === status || 500 === status || 502 === status || 503 === status || 504 === status;
|
|
7
|
+
}
|
|
8
|
+
function ingestResponseAccepted(status) {
|
|
9
|
+
return status >= 200 && status < 300 || 409 === status;
|
|
10
|
+
}
|
|
1
11
|
class DuckBugService {
|
|
2
|
-
|
|
12
|
+
logsUrl;
|
|
13
|
+
errorsUrl;
|
|
14
|
+
logsBatchUrl;
|
|
15
|
+
errorsBatchUrl;
|
|
16
|
+
maxBatchSize;
|
|
17
|
+
maxRetries;
|
|
18
|
+
retryDelayMs;
|
|
19
|
+
fetchImpl;
|
|
20
|
+
onTransportError;
|
|
21
|
+
logQueue = [];
|
|
22
|
+
errorQueue = [];
|
|
23
|
+
logChain = Promise.resolve();
|
|
24
|
+
errorChain = Promise.resolve();
|
|
3
25
|
constructor(config){
|
|
4
|
-
|
|
26
|
+
const parsed = parseDuckBugIngestDsn(config.dsn);
|
|
27
|
+
this.logsUrl = ingestLogsUrl(parsed);
|
|
28
|
+
this.errorsUrl = ingestErrorsUrl(parsed);
|
|
29
|
+
this.logsBatchUrl = ingestLogsBatchUrl(parsed);
|
|
30
|
+
this.errorsBatchUrl = ingestErrorsBatchUrl(parsed);
|
|
31
|
+
this.maxBatchSize = config.transport?.maxBatchSize ?? 1;
|
|
32
|
+
this.maxRetries = config.transport?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
33
|
+
this.retryDelayMs = config.transport?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
34
|
+
this.fetchImpl = config.transport?.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
35
|
+
this.onTransportError = config.onTransportError;
|
|
5
36
|
}
|
|
6
37
|
sendLog(logInfo) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
38
|
+
this.enqueueLogTransport(async ()=>{
|
|
39
|
+
this.logQueue.push(logInfo);
|
|
40
|
+
await this.pumpLogsAfterEnqueue();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
sendError(errorRequest) {
|
|
44
|
+
this.enqueueErrorTransport(async ()=>{
|
|
45
|
+
this.errorQueue.push(errorRequest);
|
|
46
|
+
await this.pumpErrorsAfterEnqueue();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
flush() {
|
|
50
|
+
return Promise.all([
|
|
51
|
+
this.flushLogs(),
|
|
52
|
+
this.flushErrors()
|
|
53
|
+
]).then(()=>{});
|
|
54
|
+
}
|
|
55
|
+
flushLogs() {
|
|
56
|
+
return this.enqueueLogTransport(async ()=>{
|
|
57
|
+
while(this.logQueue.length > 0)await this.drainLogStep();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
flushErrors() {
|
|
61
|
+
return this.enqueueErrorTransport(async ()=>{
|
|
62
|
+
while(this.errorQueue.length > 0)await this.drainErrorStep();
|
|
13
63
|
});
|
|
14
64
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
65
|
+
enqueueLogTransport(fn) {
|
|
66
|
+
const p = this.logChain.then(()=>fn());
|
|
67
|
+
this.logChain = p.then(()=>void 0, ()=>void 0);
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
enqueueErrorTransport(fn) {
|
|
71
|
+
const p = this.errorChain.then(()=>fn());
|
|
72
|
+
this.errorChain = p.then(()=>void 0, ()=>void 0);
|
|
73
|
+
return p;
|
|
74
|
+
}
|
|
75
|
+
async pumpLogsAfterEnqueue() {
|
|
76
|
+
if (this.maxBatchSize <= 1) {
|
|
77
|
+
const item = this.logQueue.shift();
|
|
78
|
+
if (item) await this.postJsonWithRetry(this.logsUrl, item, "log", 1);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (this.logQueue.length >= this.maxBatchSize) {
|
|
82
|
+
const n = Math.min(this.maxBatchSize, MAX_BACKEND_BATCH, this.logQueue.length);
|
|
83
|
+
const batch = this.logQueue.splice(0, n);
|
|
84
|
+
await this.postJsonWithRetry(this.logsBatchUrl, batch, "log", batch.length);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async pumpErrorsAfterEnqueue() {
|
|
88
|
+
if (this.maxBatchSize <= 1) {
|
|
89
|
+
const item = this.errorQueue.shift();
|
|
90
|
+
if (item) await this.postJsonWithRetry(this.errorsUrl, item, "error", 1);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (this.errorQueue.length >= this.maxBatchSize) {
|
|
94
|
+
const n = Math.min(this.maxBatchSize, MAX_BACKEND_BATCH, this.errorQueue.length);
|
|
95
|
+
const batch = this.errorQueue.splice(0, n);
|
|
96
|
+
await this.postJsonWithRetry(this.errorsBatchUrl, batch, "error", batch.length);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async drainLogStep() {
|
|
100
|
+
if (this.maxBatchSize <= 1) {
|
|
101
|
+
const item = this.logQueue.shift();
|
|
102
|
+
if (item) await this.postJsonWithRetry(this.logsUrl, item, "log", 1);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const n = Math.min(this.maxBatchSize, MAX_BACKEND_BATCH, this.logQueue.length);
|
|
106
|
+
if (0 === n) return;
|
|
107
|
+
const batch = this.logQueue.splice(0, n);
|
|
108
|
+
await this.postJsonWithRetry(this.logsBatchUrl, batch, "log", batch.length);
|
|
109
|
+
}
|
|
110
|
+
async drainErrorStep() {
|
|
111
|
+
if (this.maxBatchSize <= 1) {
|
|
112
|
+
const item = this.errorQueue.shift();
|
|
113
|
+
if (item) await this.postJsonWithRetry(this.errorsUrl, item, "error", 1);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const n = Math.min(this.maxBatchSize, MAX_BACKEND_BATCH, this.errorQueue.length);
|
|
117
|
+
if (0 === n) return;
|
|
118
|
+
const batch = this.errorQueue.splice(0, n);
|
|
119
|
+
await this.postJsonWithRetry(this.errorsBatchUrl, batch, "error", batch.length);
|
|
120
|
+
}
|
|
121
|
+
emitFailure(info) {
|
|
122
|
+
this.onTransportError?.(info);
|
|
123
|
+
}
|
|
124
|
+
async postJsonWithRetry(url, body, kind, itemCount) {
|
|
125
|
+
let lastErr = new Error("unknown transport failure");
|
|
126
|
+
const maxAttempts = this.maxRetries + 1;
|
|
127
|
+
for(let attempt = 0; attempt < maxAttempts; attempt++){
|
|
128
|
+
try {
|
|
129
|
+
const res = await this.fetchImpl(url, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
"Content-Type": "application/json"
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify(body)
|
|
135
|
+
});
|
|
136
|
+
if (ingestResponseAccepted(res.status)) return;
|
|
137
|
+
let detail = "";
|
|
138
|
+
try {
|
|
139
|
+
const t = await res.text();
|
|
140
|
+
const trimmed = t.trim();
|
|
141
|
+
if (trimmed) {
|
|
142
|
+
const max = 400;
|
|
143
|
+
detail = trimmed.length > max ? `${trimmed.slice(0, max)}\u{2026}` : trimmed;
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
lastErr = new Error(detail ? `ingest HTTP ${res.status}: ${detail}` : `ingest HTTP ${res.status}`);
|
|
147
|
+
if (!isRetriableHttpStatus(res.status)) break;
|
|
148
|
+
} catch (e) {
|
|
149
|
+
lastErr = e;
|
|
150
|
+
}
|
|
151
|
+
if (attempt < maxAttempts - 1) {
|
|
152
|
+
const delay = this.retryDelayMs * 2 ** attempt;
|
|
153
|
+
await new Promise((r)=>setTimeout(r, delay));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const message = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
157
|
+
this.emitFailure({
|
|
158
|
+
kind,
|
|
159
|
+
itemCount,
|
|
160
|
+
attempts: maxAttempts,
|
|
161
|
+
error: lastErr,
|
|
162
|
+
message
|
|
22
163
|
});
|
|
23
164
|
}
|
|
24
165
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function randomEventId() {
|
|
2
|
+
const c = globalThis.crypto;
|
|
3
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
4
|
+
throw new Error("crypto.randomUUID is not available; use a runtime with Web Crypto or set eventId on events explicitly");
|
|
5
|
+
}
|
|
6
|
+
function ensureEventId(event) {
|
|
7
|
+
if (void 0 !== event.eventId && "" !== event.eventId) return event;
|
|
8
|
+
return {
|
|
9
|
+
...event,
|
|
10
|
+
eventId: randomEventId()
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export { ensureEventId };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SDK_IDENTITY } from "../sdkIdentity.js";
|
|
2
|
+
import { ensureEventId } from "./ensureEventId.js";
|
|
3
|
+
import { sanitizeIngestPayload } from "./sanitizeIngestPayload.js";
|
|
4
|
+
import { stripIngestSections } from "./stripIngestSections.js";
|
|
5
|
+
function withDefaultSdk(event) {
|
|
6
|
+
if (void 0 !== event.sdk) return event;
|
|
7
|
+
return {
|
|
8
|
+
...event,
|
|
9
|
+
sdk: {
|
|
10
|
+
...SDK_IDENTITY
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function finalizeIngestEvent(event, options = {}) {
|
|
15
|
+
const withSdk = withDefaultSdk(event);
|
|
16
|
+
const stripped = stripIngestSections(withSdk, options.stripSections);
|
|
17
|
+
const sanitized = sanitizeIngestPayload(stripped, options.extraSensitiveKeys);
|
|
18
|
+
return ensureEventId(sanitized);
|
|
19
|
+
}
|
|
20
|
+
export { finalizeIngestEvent };
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import { DuckBugProvider } from "./DuckBugProvider.js";
|
|
2
|
-
|
|
2
|
+
import { ensureEventId } from "./ensureEventId.js";
|
|
3
|
+
import { finalizeIngestEvent } from "./finalizeIngestEvent.js";
|
|
4
|
+
import { Pond } from "./Pond.js";
|
|
5
|
+
import { stripIngestSections } from "./stripIngestSections.js";
|
|
6
|
+
export { DuckBugProvider, Pond, ensureEventId, finalizeIngestEvent, stripIngestSections };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function parseDuckBugIngestDsn(dsn) {
|
|
2
|
+
let url;
|
|
3
|
+
try {
|
|
4
|
+
url = new URL(dsn);
|
|
5
|
+
} catch {
|
|
6
|
+
throw new Error("Invalid DuckBug DSN: not a valid URL");
|
|
7
|
+
}
|
|
8
|
+
const match = url.pathname.match(/^((?:\/api)?\/ingest)\/([^/]+)$/);
|
|
9
|
+
if (!match) throw new Error("Invalid DuckBug DSN: path must be /ingest/{projectId}:{publicKey} or /api/ingest/{projectId}:{publicKey}");
|
|
10
|
+
const ingestPathPrefix = match[1];
|
|
11
|
+
const segment = match[2];
|
|
12
|
+
const colon = segment.indexOf(":");
|
|
13
|
+
if (colon <= 0 || colon === segment.length - 1) throw new Error("Invalid DuckBug DSN: ingest segment must be projectId:publicKey");
|
|
14
|
+
return {
|
|
15
|
+
origin: url.origin,
|
|
16
|
+
ingestPathPrefix,
|
|
17
|
+
projectId: segment.slice(0, colon),
|
|
18
|
+
key: segment.slice(colon + 1)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function ingestBase(parsed) {
|
|
22
|
+
return `${parsed.origin}${parsed.ingestPathPrefix}/${parsed.projectId}:${parsed.key}`;
|
|
23
|
+
}
|
|
24
|
+
function ingestLogsUrl(parsed) {
|
|
25
|
+
return `${ingestBase(parsed)}/logs`;
|
|
26
|
+
}
|
|
27
|
+
function ingestErrorsUrl(parsed) {
|
|
28
|
+
return `${ingestBase(parsed)}/errors`;
|
|
29
|
+
}
|
|
30
|
+
function ingestLogsBatchUrl(parsed) {
|
|
31
|
+
return `${ingestBase(parsed)}/logs/batch`;
|
|
32
|
+
}
|
|
33
|
+
function ingestErrorsBatchUrl(parsed) {
|
|
34
|
+
return `${ingestBase(parsed)}/errors/batch`;
|
|
35
|
+
}
|
|
36
|
+
export { ingestErrorsBatchUrl, ingestErrorsUrl, ingestLogsBatchUrl, ingestLogsUrl, parseDuckBugIngestDsn };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const DEFAULT_SENSITIVE_KEYS = new Set([
|
|
2
|
+
"password",
|
|
3
|
+
"token",
|
|
4
|
+
"api_key",
|
|
5
|
+
"authorization",
|
|
6
|
+
"cookie",
|
|
7
|
+
"session",
|
|
8
|
+
"secret"
|
|
9
|
+
]);
|
|
10
|
+
const MASK = "***";
|
|
11
|
+
function normalizeKey(key) {
|
|
12
|
+
return key.trim().toLowerCase().replace(/-/g, "_").replace(/\s+/g, "_");
|
|
13
|
+
}
|
|
14
|
+
function isSensitive(key, extra) {
|
|
15
|
+
const n = normalizeKey(key);
|
|
16
|
+
if (DEFAULT_SENSITIVE_KEYS.has(n)) return true;
|
|
17
|
+
for (const e of extra)if (normalizeKey(e) === n) return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function cloneValue(value, extraSensitive, depth) {
|
|
21
|
+
if (depth > 20) return value;
|
|
22
|
+
if (null === value || "object" != typeof value) return value;
|
|
23
|
+
if (Array.isArray(value)) return value.map((v)=>cloneValue(v, extraSensitive, depth + 1));
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [k, v] of Object.entries(value))if (isSensitive(k, extraSensitive)) out[k] = MASK;
|
|
26
|
+
else out[k] = cloneValue(v, extraSensitive, depth + 1);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function sanitizeIngestPayload(payload, extraSensitiveKeys) {
|
|
30
|
+
const extra = new Set(extraSensitiveKeys ?? []);
|
|
31
|
+
return cloneValue(JSON.parse(JSON.stringify(payload)), extra, 0);
|
|
32
|
+
}
|
|
33
|
+
export { sanitizeIngestPayload };
|
|
File without changes
|
package/dist/esm/SDK/DuckSDK.js
CHANGED
|
@@ -1,28 +1,144 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { processError } from "../DuckBug/DuckBugHelper.js";
|
|
2
|
+
import { finalizeIngestEvent } from "../DuckBug/finalizeIngestEvent.js";
|
|
3
|
+
import { SDK_IDENTITY } from "../sdkIdentity.js";
|
|
4
|
+
import { logLevel } from "./LogLevel.js";
|
|
5
|
+
import { LogProvider } from "./LogProvider.js";
|
|
6
|
+
function isPromiseLike(v) {
|
|
7
|
+
return null !== v && "object" == typeof v && "then" in v && "function" == typeof v.then;
|
|
8
|
+
}
|
|
2
9
|
class DuckSDK {
|
|
3
10
|
providers;
|
|
4
|
-
|
|
11
|
+
scope = {};
|
|
12
|
+
beforeSendHook;
|
|
13
|
+
extraSensitiveKeys;
|
|
14
|
+
stripSections;
|
|
15
|
+
constructor(providers, logProviderConfig, options){
|
|
5
16
|
this.providers = providers;
|
|
17
|
+
this.beforeSendHook = options?.beforeSend;
|
|
18
|
+
this.extraSensitiveKeys = options?.extraSensitiveKeys;
|
|
19
|
+
this.stripSections = options?.stripSections;
|
|
6
20
|
new LogProvider(providers, logProviderConfig);
|
|
7
21
|
}
|
|
22
|
+
setScope(scope) {
|
|
23
|
+
this.scope = {
|
|
24
|
+
...this.scope,
|
|
25
|
+
...scope
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
flush() {
|
|
29
|
+
return Promise.all(this.providers.map((p)=>{
|
|
30
|
+
const f = p.flush;
|
|
31
|
+
return f ? Promise.resolve(f.call(p)) : Promise.resolve();
|
|
32
|
+
})).then(()=>void 0);
|
|
33
|
+
}
|
|
8
34
|
log(tag, payload) {
|
|
9
|
-
this.
|
|
35
|
+
this.emitLog(logLevel.DEBUG, tag, payload);
|
|
10
36
|
}
|
|
11
37
|
error(tag, payload) {
|
|
12
|
-
this.
|
|
38
|
+
this.emitLog(logLevel.ERROR, tag, payload);
|
|
13
39
|
}
|
|
14
40
|
debug(tag, payload) {
|
|
15
|
-
this.
|
|
41
|
+
this.emitLog(logLevel.DEBUG, tag, payload);
|
|
16
42
|
}
|
|
17
43
|
warn(tag, payload) {
|
|
18
|
-
this.
|
|
44
|
+
this.emitLog(logLevel.WARN, tag, payload);
|
|
19
45
|
}
|
|
20
46
|
fatal(tag, payload) {
|
|
21
|
-
this.
|
|
47
|
+
this.emitLog(logLevel.FATAL, tag, payload);
|
|
48
|
+
}
|
|
49
|
+
quack(tag, error) {
|
|
50
|
+
const time = Date.now();
|
|
51
|
+
const built = processError(error, tag, time);
|
|
52
|
+
const merged = this.mergeScope(built);
|
|
53
|
+
const finalized = finalizeIngestEvent(merged, {
|
|
54
|
+
extraSensitiveKeys: this.extraSensitiveKeys,
|
|
55
|
+
stripSections: this.stripSections
|
|
56
|
+
});
|
|
57
|
+
if (!this.beforeSendHook) {
|
|
58
|
+
for (const p of this.providers)p.sendError(finalized, {
|
|
59
|
+
skipPrivacyPipeline: true
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const arg = {
|
|
64
|
+
kind: "error",
|
|
65
|
+
event: finalized
|
|
66
|
+
};
|
|
67
|
+
const out = this.beforeSendHook(arg);
|
|
68
|
+
if (isPromiseLike(out)) return void out.then((resolved)=>{
|
|
69
|
+
const after = this.normalizeBeforeSendError(resolved, finalized);
|
|
70
|
+
if (null === after) return;
|
|
71
|
+
for (const p of this.providers)p.sendError(after, {
|
|
72
|
+
skipPrivacyPipeline: true
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
const after = this.normalizeBeforeSendError(out, finalized);
|
|
76
|
+
if (null === after) return;
|
|
77
|
+
for (const p of this.providers)p.sendError(after, {
|
|
78
|
+
skipPrivacyPipeline: true
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
normalizeBeforeSendLog(out, fallback) {
|
|
82
|
+
if (null === out) return null;
|
|
83
|
+
if (void 0 === out) return fallback;
|
|
84
|
+
return out;
|
|
22
85
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
86
|
+
normalizeBeforeSendError(out, fallback) {
|
|
87
|
+
if (null === out) return null;
|
|
88
|
+
if (void 0 === out) return fallback;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
emitLog(level, message, payload) {
|
|
92
|
+
const event = this.mergeScope({
|
|
93
|
+
time: Date.now(),
|
|
94
|
+
level,
|
|
95
|
+
message,
|
|
96
|
+
platform: "node",
|
|
97
|
+
sdk: {
|
|
98
|
+
...SDK_IDENTITY
|
|
99
|
+
},
|
|
100
|
+
...void 0 !== payload ? {
|
|
101
|
+
context: payload
|
|
102
|
+
} : {}
|
|
103
|
+
});
|
|
104
|
+
const finalized = finalizeIngestEvent(event, {
|
|
105
|
+
extraSensitiveKeys: this.extraSensitiveKeys,
|
|
106
|
+
stripSections: this.stripSections
|
|
107
|
+
});
|
|
108
|
+
if (!this.beforeSendHook) {
|
|
109
|
+
for (const p of this.providers)p.sendLog(finalized, {
|
|
110
|
+
skipPrivacyPipeline: true
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const arg = {
|
|
115
|
+
kind: "log",
|
|
116
|
+
event: finalized
|
|
117
|
+
};
|
|
118
|
+
const out = this.beforeSendHook(arg);
|
|
119
|
+
if (isPromiseLike(out)) return void out.then((resolved)=>{
|
|
120
|
+
const after = this.normalizeBeforeSendLog(resolved, finalized);
|
|
121
|
+
if (null === after) return;
|
|
122
|
+
for (const p of this.providers)p.sendLog(after, {
|
|
123
|
+
skipPrivacyPipeline: true
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
const after = this.normalizeBeforeSendLog(out, finalized);
|
|
127
|
+
if (null === after) return;
|
|
128
|
+
for (const p of this.providers)p.sendLog(after, {
|
|
129
|
+
skipPrivacyPipeline: true
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
mergeScope(event) {
|
|
133
|
+
return {
|
|
134
|
+
...this.scope,
|
|
135
|
+
...event
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
class Duck extends DuckSDK {
|
|
140
|
+
captureException(error, tag) {
|
|
141
|
+
this.quack(tag ?? "error", error);
|
|
26
142
|
}
|
|
27
143
|
}
|
|
28
|
-
export { DuckSDK };
|
|
144
|
+
export { Duck, DuckSDK };
|
package/dist/esm/SDK/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DuckSDK } from "./DuckSDK.js";
|
|
1
|
+
import { Duck, DuckSDK } from "./DuckSDK.js";
|
|
2
2
|
import { logLevel } from "./LogLevel.js";
|
|
3
3
|
import { LogProvider } from "./LogProvider.js";
|
|
4
|
-
export { DuckSDK, LogProvider, logLevel };
|
|
4
|
+
export { Duck, DuckSDK, LogProvider, logLevel };
|
|
File without changes
|
|
File without changes
|
package/dist/esm/index.js
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function registerNodeGlobalErrorHandlers(options) {
|
|
2
|
+
const rejectionTag = options.rejectionTag ?? "unhandledRejection";
|
|
3
|
+
const exceptionTag = options.exceptionTag ?? "uncaughtException";
|
|
4
|
+
const onRejection = (reason)=>{
|
|
5
|
+
const err = reason instanceof Error ? reason : new Error("string" == typeof reason ? reason : JSON.stringify(reason));
|
|
6
|
+
options.duck.quack(rejectionTag, err);
|
|
7
|
+
options.duck.flush();
|
|
8
|
+
};
|
|
9
|
+
const onException = (err)=>{
|
|
10
|
+
options.duck.quack(exceptionTag, err);
|
|
11
|
+
options.duck.flush();
|
|
12
|
+
};
|
|
13
|
+
process.on("unhandledRejection", onRejection);
|
|
14
|
+
process.on("uncaughtException", onException);
|
|
15
|
+
return ()=>{
|
|
16
|
+
process.off("unhandledRejection", onRejection);
|
|
17
|
+
process.off("uncaughtException", onException);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export { registerNodeGlobalErrorHandlers };
|
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
import type { DuckBugErrorEvent, DuckBugLogEvent } from "../contract";
|
|
2
|
+
import type { StrippableIngestSection } from "./stripIngestSections";
|
|
3
|
+
import type { TransportFailureInfo } from "./transportTypes";
|
|
4
|
+
export type BeforeSendIngestArg = {
|
|
5
|
+
kind: "log";
|
|
6
|
+
event: DuckBugLogEvent;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "error";
|
|
9
|
+
event: DuckBugErrorEvent;
|
|
10
|
+
};
|
|
11
|
+
export type BeforeSendIngestResult = DuckBugLogEvent | DuckBugErrorEvent | null | undefined;
|
|
12
|
+
export type DuckBugTransportConfig = {
|
|
13
|
+
/** When <= 1, each event is POSTed to single-event ingest. When > 1, batches use /batch until flush. Default 1. */
|
|
14
|
+
maxBatchSize?: number;
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
/** Base delay; attempts use exponential backoff from this value. */
|
|
17
|
+
retryDelayMs?: number;
|
|
18
|
+
fetchImpl?: typeof fetch;
|
|
19
|
+
};
|
|
1
20
|
export type DuckBugConfig = {
|
|
2
21
|
dsn: string;
|
|
22
|
+
extraSensitiveKeys?: string[];
|
|
23
|
+
stripSections?: StrippableIngestSection[];
|
|
24
|
+
/**
|
|
25
|
+
* Runs after strip/sanitize/eventId for direct `DuckBugProvider` sends.
|
|
26
|
+
* When using `DuckSDK`, prefer `DuckSDK` constructor `beforeSend`.
|
|
27
|
+
*/
|
|
28
|
+
beforeSend?: (arg: BeforeSendIngestArg) => BeforeSendIngestResult | Promise<BeforeSendIngestResult>;
|
|
29
|
+
transport?: DuckBugTransportConfig;
|
|
30
|
+
onTransportError?: (info: TransportFailureInfo) => void;
|
|
3
31
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DuckBugErrorEvent } from "../contract";
|
|
2
|
+
export type StacktraceFrame = {
|
|
3
|
+
index: number;
|
|
4
|
+
content: string;
|
|
5
|
+
};
|
|
6
|
+
export type Stacktrace = {
|
|
7
|
+
raw: string;
|
|
8
|
+
frames: StacktraceFrame[];
|
|
9
|
+
};
|
|
10
|
+
type ParsedError = {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
stacktrace: Stacktrace;
|
|
14
|
+
context: unknown;
|
|
15
|
+
};
|
|
16
|
+
export declare function parseError(error: Error): ParsedError;
|
|
17
|
+
/**
|
|
18
|
+
* Builds a canonical DuckBug error event: {@link error.message} is the primary
|
|
19
|
+
* `message` field; `tag` is sent as `dTags` for grouping/search.
|
|
20
|
+
*/
|
|
21
|
+
export declare function processError(error: Error, tag: string, time: number): DuckBugErrorEvent;
|
|
22
|
+
export {};
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { DuckBugErrorEvent, DuckBugLogEvent } from "../contract";
|
|
2
|
+
import { type LogLevel, type Provider, type SendEventMeta } from "../SDK";
|
|
2
3
|
import type { DuckBugConfig } from "./DuckBugConfig";
|
|
3
4
|
import { DuckBugService } from "./DuckBugService";
|
|
4
5
|
export declare class DuckBugProvider implements Provider {
|
|
5
6
|
service: DuckBugService;
|
|
7
|
+
private readonly ingestConfig;
|
|
6
8
|
constructor(config: DuckBugConfig);
|
|
9
|
+
static fromDSN(dsn: string): DuckBugProvider;
|
|
10
|
+
/** Drains the ingest transport queue; safe to call multiple times. */
|
|
11
|
+
flush(): Promise<void>;
|
|
12
|
+
sendLog(event: DuckBugLogEvent, meta?: SendEventMeta): void;
|
|
13
|
+
sendError(event: DuckBugErrorEvent, meta?: SendEventMeta): void;
|
|
14
|
+
private sendLogAsync;
|
|
15
|
+
private sendErrorAsync;
|
|
7
16
|
warn(...args: unknown[]): void;
|
|
8
17
|
error(...args: unknown[]): void;
|
|
9
18
|
log(...args: unknown[]): void;
|
|
19
|
+
/** @deprecated Prefer {@link Duck} logging methods; kept for direct provider use. */
|
|
10
20
|
report(tag: string, level: LogLevel, payload?: object): void;
|
|
11
21
|
quack(tag: string, error: Error): void;
|
|
22
|
+
private normalizedContextPayload;
|
|
12
23
|
private convertArgsToString;
|
|
13
24
|
private getTimeStamp;
|
|
14
25
|
}
|
|
@@ -1,12 +1,34 @@
|
|
|
1
|
+
import type { DuckBugErrorEvent, DuckBugLogEvent } from "../contract";
|
|
1
2
|
import type { DuckBugConfig } from "./DuckBugConfig";
|
|
2
|
-
|
|
3
|
+
/** @deprecated Use {@link DuckBugErrorEvent} from the public contract types. */
|
|
4
|
+
export type ErrorRequest = DuckBugErrorEvent;
|
|
3
5
|
export declare class DuckBugService {
|
|
4
|
-
private
|
|
6
|
+
private readonly logsUrl;
|
|
7
|
+
private readonly errorsUrl;
|
|
8
|
+
private readonly logsBatchUrl;
|
|
9
|
+
private readonly errorsBatchUrl;
|
|
10
|
+
private readonly maxBatchSize;
|
|
11
|
+
private readonly maxRetries;
|
|
12
|
+
private readonly retryDelayMs;
|
|
13
|
+
private readonly fetchImpl;
|
|
14
|
+
private readonly onTransportError?;
|
|
15
|
+
private readonly logQueue;
|
|
16
|
+
private readonly errorQueue;
|
|
17
|
+
private logChain;
|
|
18
|
+
private errorChain;
|
|
5
19
|
constructor(config: DuckBugConfig);
|
|
6
|
-
sendLog(logInfo:
|
|
7
|
-
sendError(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
sendLog(logInfo: DuckBugLogEvent): void;
|
|
21
|
+
sendError(errorRequest: DuckBugErrorEvent): void;
|
|
22
|
+
/** Drains queued events for both logs and errors. Safe to call multiple times. */
|
|
23
|
+
flush(): Promise<void>;
|
|
24
|
+
private flushLogs;
|
|
25
|
+
private flushErrors;
|
|
26
|
+
private enqueueLogTransport;
|
|
27
|
+
private enqueueErrorTransport;
|
|
28
|
+
private pumpLogsAfterEnqueue;
|
|
29
|
+
private pumpErrorsAfterEnqueue;
|
|
30
|
+
private drainLogStep;
|
|
31
|
+
private drainErrorStep;
|
|
32
|
+
private emitFailure;
|
|
33
|
+
private postJsonWithRetry;
|
|
12
34
|
}
|