@duckbug/js 0.1.3 → 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.
Files changed (61) hide show
  1. package/README.md +348 -73
  2. package/dist/cjs/DuckBug/DuckBugHelper.cjs +136 -0
  3. package/dist/cjs/DuckBug/DuckBugProvider.cjs +82 -13
  4. package/dist/cjs/DuckBug/DuckBugService.cjs +158 -17
  5. package/dist/cjs/DuckBug/Pond.cjs +44 -0
  6. package/dist/cjs/DuckBug/ensureEventId.cjs +47 -0
  7. package/dist/cjs/DuckBug/finalizeIngestEvent.cjs +54 -0
  8. package/dist/cjs/DuckBug/index.cjs +18 -2
  9. package/dist/cjs/DuckBug/parseDuckBugDsn.cjs +82 -0
  10. package/dist/cjs/DuckBug/sanitizeIngestPayload.cjs +67 -0
  11. package/dist/cjs/DuckBug/stripIngestSections.cjs +43 -0
  12. package/dist/cjs/DuckBug/transportTypes.cjs +18 -0
  13. package/dist/cjs/SDK/DuckSDK.cjs +129 -12
  14. package/dist/cjs/SDK/index.cjs +5 -2
  15. package/dist/cjs/contract/index.cjs +18 -0
  16. package/dist/cjs/contract/ingestEvents.cjs +18 -0
  17. package/dist/cjs/index.cjs +24 -6
  18. package/dist/cjs/integrations/node.cjs +54 -0
  19. package/dist/cjs/sdkIdentity.cjs +47 -0
  20. package/dist/esm/DuckBug/DuckBugHelper.js +99 -0
  21. package/dist/esm/DuckBug/DuckBugProvider.js +82 -13
  22. package/dist/esm/DuckBug/DuckBugService.js +156 -15
  23. package/dist/esm/DuckBug/Pond.js +10 -0
  24. package/dist/esm/DuckBug/ensureEventId.js +13 -0
  25. package/dist/esm/DuckBug/finalizeIngestEvent.js +20 -0
  26. package/dist/esm/DuckBug/index.js +5 -1
  27. package/dist/esm/DuckBug/parseDuckBugDsn.js +36 -0
  28. package/dist/esm/DuckBug/sanitizeIngestPayload.js +33 -0
  29. package/dist/esm/DuckBug/stripIngestSections.js +9 -0
  30. package/dist/esm/DuckBug/transportTypes.js +0 -0
  31. package/dist/esm/SDK/DuckSDK.js +125 -11
  32. package/dist/esm/SDK/index.js +2 -2
  33. package/dist/esm/contract/index.js +0 -0
  34. package/dist/esm/contract/ingestEvents.js +0 -0
  35. package/dist/esm/index.js +2 -0
  36. package/dist/esm/integrations/node.js +20 -0
  37. package/dist/esm/sdkIdentity.js +7 -0
  38. package/dist/types/DuckBug/DuckBugConfig.d.ts +28 -0
  39. package/dist/types/DuckBug/DuckBugHelper.d.ts +22 -0
  40. package/dist/types/DuckBug/DuckBugProvider.d.ts +12 -1
  41. package/dist/types/DuckBug/DuckBugService.d.ts +30 -8
  42. package/dist/types/DuckBug/Log.d.ts +1 -7
  43. package/dist/types/DuckBug/Pond.d.ts +9 -0
  44. package/dist/types/DuckBug/ensureEventId.d.ts +4 -0
  45. package/dist/types/DuckBug/finalizeIngestEvent.d.ts +11 -0
  46. package/dist/types/DuckBug/index.d.ts +7 -1
  47. package/dist/types/DuckBug/parseDuckBugDsn.d.ts +17 -0
  48. package/dist/types/DuckBug/sanitizeIngestPayload.d.ts +4 -0
  49. package/dist/types/DuckBug/stripIngestSections.d.ts +4 -0
  50. package/dist/types/DuckBug/transportTypes.d.ts +7 -0
  51. package/dist/types/SDK/DuckSDK.d.ts +30 -3
  52. package/dist/types/SDK/Provider.d.ts +11 -3
  53. package/dist/types/SDK/index.d.ts +3 -2
  54. package/dist/types/contract/index.d.ts +1 -0
  55. package/dist/types/contract/ingestEvents.d.ts +58 -0
  56. package/dist/types/index.d.ts +2 -0
  57. package/dist/types/integrations/node.d.ts +12 -0
  58. package/dist/types/sdkIdentity.d.ts +7 -0
  59. package/package.json +23 -7
  60. package/schemas/error-event.schema.json +147 -0
  61. 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
- config;
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
- this.config = config;
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
- fetch(`${this.config.dsn}/logs`, {
8
- method: "POST",
9
- headers: {
10
- "Content-Type": "application/json"
11
- },
12
- body: JSON.stringify(logInfo)
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
- sendError(errorInfo) {
16
- fetch(`${this.config.dsn}/errors`, {
17
- method: "POST",
18
- headers: {
19
- "Content-Type": "application/json"
20
- },
21
- body: JSON.stringify(errorInfo)
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,10 @@
1
+ const Pond = {
2
+ ripple (extraSensitiveKeys = []) {
3
+ return {
4
+ extraSensitiveKeys: [
5
+ ...extraSensitiveKeys
6
+ ]
7
+ };
8
+ }
9
+ };
10
+ export { Pond };
@@ -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
- export { DuckBugProvider };
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 };
@@ -0,0 +1,9 @@
1
+ function stripIngestSections(event, sections) {
2
+ if (void 0 === sections || 0 === sections.length) return event;
3
+ const o = {
4
+ ...event
5
+ };
6
+ for (const s of sections)delete o[s];
7
+ return o;
8
+ }
9
+ export { stripIngestSections };
File without changes
@@ -1,30 +1,144 @@
1
- import { LogProvider, logLevel } from "./index.js";
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
- constructor(providers, logProviderConfig){
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.sendReportToPlugins(tag, logLevel.DEBUG, payload);
35
+ this.emitLog(logLevel.DEBUG, tag, payload);
10
36
  }
11
37
  error(tag, payload) {
12
- this.sendReportToPlugins(tag, logLevel.DEBUG, payload);
38
+ this.emitLog(logLevel.ERROR, tag, payload);
13
39
  }
14
40
  debug(tag, payload) {
15
- this.sendReportToPlugins(tag, logLevel.DEBUG, payload);
41
+ this.emitLog(logLevel.DEBUG, tag, payload);
16
42
  }
17
43
  warn(tag, payload) {
18
- this.sendReportToPlugins(tag, logLevel.WARN, payload);
44
+ this.emitLog(logLevel.WARN, tag, payload);
19
45
  }
20
46
  fatal(tag, payload) {
21
- this.sendReportToPlugins(tag, logLevel.FATAL, payload);
47
+ this.emitLog(logLevel.FATAL, tag, payload);
22
48
  }
23
49
  quack(tag, error) {
24
- this.providers.forEach((plugin)=>plugin.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;
25
85
  }
26
- sendReportToPlugins(tag, level, payload) {
27
- this.providers.forEach((plugin)=>plugin.report(tag, level, payload));
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);
28
142
  }
29
143
  }
30
- export { DuckSDK };
144
+ export { Duck, DuckSDK };
@@ -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
@@ -1,2 +1,4 @@
1
+ export * from "./contract/index.js";
1
2
  export * from "./DuckBug/index.js";
3
+ export * from "./integrations/node.js";
2
4
  export * from "./SDK/index.js";
@@ -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 };
@@ -0,0 +1,7 @@
1
+ const SDK_NAME = "@duckbug/js";
2
+ const SDK_VERSION = "0.1.3";
3
+ const SDK_IDENTITY = {
4
+ name: SDK_NAME,
5
+ version: SDK_VERSION
6
+ };
7
+ export { SDK_IDENTITY, SDK_NAME, SDK_VERSION };
@@ -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 &lt;= 1, each event is POSTed to single-event ingest. When &gt; 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 { type LogLevel, type Provider } from "../SDK";
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
- import type { Log } from "./Log";
3
+ /** @deprecated Use {@link DuckBugErrorEvent} from the public contract types. */
4
+ export type ErrorRequest = DuckBugErrorEvent;
3
5
  export declare class DuckBugService {
4
- private config;
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: Log): void;
7
- sendError(errorInfo: {
8
- message: string;
9
- stack?: string;
10
- context: string;
11
- }): void;
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
  }
@@ -1,7 +1 @@
1
- import type { LogLevel } from "../SDK/LogLevel";
2
- export type Log = {
3
- message: string;
4
- level: LogLevel;
5
- time: number;
6
- context: object | undefined;
7
- };
1
+ export type { DuckBugLogEvent as Log } from "../contract";