@aj-2000-test/goodlogs-sdk 0.1.20 → 0.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/bin/goodlogs.cjs +180 -0
- package/dist/client-w3t1Yzjd.d.cts +231 -0
- package/dist/client-w3t1Yzjd.d.ts +231 -0
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +431 -93
- package/dist/index.d.ts +431 -93
- package/dist/index.js +6 -3
- package/dist/replay.cjs +2 -0
- package/dist/replay.d.cts +70 -0
- package/dist/replay.d.ts +70 -0
- package/dist/replay.js +2 -0
- package/package.json +13 -3
package/bin/goodlogs.cjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* goodlogs CLI — small helper for release-time tasks.
|
|
4
|
+
*
|
|
5
|
+
* Currently supported:
|
|
6
|
+
* goodlogs upload-sourcemaps --release <name> --api-key <gl_sk_...> [--endpoint URL] <dir-or-file>...
|
|
7
|
+
*
|
|
8
|
+
* Walks each path, finds *.js.map files, and PUTs them to the GoodLogs API.
|
|
9
|
+
* Honours the //# debugId=<uuid> comment in the matching .js file when
|
|
10
|
+
* present so symbolication survives URL changes.
|
|
11
|
+
*
|
|
12
|
+
* Designed to be stable + dependency-free so it can run inside any CI pipeline
|
|
13
|
+
* without extra installs. Node 18+ required (uses fetch).
|
|
14
|
+
*/
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const out = { _: [], release: null, apiKey: null, endpoint: "https://api.goodlogs.io", verbose: false };
|
|
22
|
+
for (let i = 2; i < argv.length; i++) {
|
|
23
|
+
const a = argv[i];
|
|
24
|
+
if (a === "--release") out.release = argv[++i];
|
|
25
|
+
else if (a === "--api-key") out.apiKey = argv[++i];
|
|
26
|
+
else if (a === "--endpoint") out.endpoint = argv[++i];
|
|
27
|
+
else if (a === "-v" || a === "--verbose") out.verbose = true;
|
|
28
|
+
else if (a === "--help" || a === "-h") { printHelp(); process.exit(0); }
|
|
29
|
+
else out._.push(a);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printHelp() {
|
|
35
|
+
process.stdout.write([
|
|
36
|
+
"goodlogs — release helpers",
|
|
37
|
+
"",
|
|
38
|
+
"Usage:",
|
|
39
|
+
" goodlogs upload-sourcemaps --release <name> --api-key <gl_sk_...> [--endpoint URL] <path>...",
|
|
40
|
+
"",
|
|
41
|
+
" --release Release identifier (e.g. v1.2.3 or commit SHA)",
|
|
42
|
+
" --api-key Secret API key with 'ingest' scope",
|
|
43
|
+
" --endpoint API endpoint (default https://api.goodlogs.io)",
|
|
44
|
+
" -v Verbose output",
|
|
45
|
+
" -h Show help",
|
|
46
|
+
"",
|
|
47
|
+
"Env: GOODLOGS_API_KEY, GOODLOGS_ENDPOINT, GOODLOGS_RELEASE override CLI flags.",
|
|
48
|
+
"",
|
|
49
|
+
].join("\n"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Recursively yield .map files under a path. */
|
|
53
|
+
function* findMaps(root) {
|
|
54
|
+
const stat = fs.statSync(root);
|
|
55
|
+
if (stat.isFile()) {
|
|
56
|
+
if (root.endsWith(".map")) yield root;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const ent of fs.readdirSync(root, { withFileTypes: true })) {
|
|
60
|
+
if (ent.name === "node_modules" || ent.name.startsWith(".")) continue;
|
|
61
|
+
const p = path.join(root, ent.name);
|
|
62
|
+
if (ent.isDirectory()) yield* findMaps(p);
|
|
63
|
+
else if (ent.name.endsWith(".map")) yield p;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Read the //# debugId=<uuid> comment from the matching .js (or the .map's
|
|
68
|
+
* embedded `debugId` field, written by some bundlers). Returns null when
|
|
69
|
+
* unavailable. */
|
|
70
|
+
function extractDebugId(mapPath) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = fs.readFileSync(mapPath, "utf8");
|
|
73
|
+
// Bundlers like esbuild + sentry-cli inject debugId at the top of the map JSON.
|
|
74
|
+
try {
|
|
75
|
+
const j = JSON.parse(raw);
|
|
76
|
+
if (typeof j.debugId === "string") return j.debugId;
|
|
77
|
+
if (typeof j.debug_id === "string") return j.debug_id;
|
|
78
|
+
} catch { /* not JSON or malformed; fall through */ }
|
|
79
|
+
} catch { /* unreadable */ }
|
|
80
|
+
|
|
81
|
+
// Fall back to scanning the sibling .js for the //# debugId= comment.
|
|
82
|
+
const jsPath = mapPath.replace(/\.map$/, "");
|
|
83
|
+
try {
|
|
84
|
+
if (!fs.existsSync(jsPath)) return null;
|
|
85
|
+
// Read the last 4KB only — comments are at the file tail.
|
|
86
|
+
const fd = fs.openSync(jsPath, "r");
|
|
87
|
+
const size = fs.fstatSync(fd).size;
|
|
88
|
+
const len = Math.min(size, 4096);
|
|
89
|
+
const buf = Buffer.alloc(len);
|
|
90
|
+
fs.readSync(fd, buf, 0, len, size - len);
|
|
91
|
+
fs.closeSync(fd);
|
|
92
|
+
const tail = buf.toString("utf8");
|
|
93
|
+
const m = tail.match(/[#@]\s*debugId\s*=\s*([a-f0-9-]{32,40})/i);
|
|
94
|
+
return m ? m[1] : null;
|
|
95
|
+
} catch { return null; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function uploadOne(opts, mapPath) {
|
|
99
|
+
const body = fs.readFileSync(mapPath);
|
|
100
|
+
const filename = path.basename(mapPath);
|
|
101
|
+
const debugId = extractDebugId(mapPath);
|
|
102
|
+
const headers = {
|
|
103
|
+
"Authorization": `Bearer ${opts.apiKey}`,
|
|
104
|
+
"X-Goodlogs-Release": opts.release,
|
|
105
|
+
"X-Goodlogs-Filename": filename,
|
|
106
|
+
"Content-Type": "application/octet-stream",
|
|
107
|
+
};
|
|
108
|
+
if (debugId) headers["X-Goodlogs-Debug-Id"] = debugId;
|
|
109
|
+
|
|
110
|
+
const url = `${opts.endpoint.replace(/\/+$/, "")}/v1/artifacts`;
|
|
111
|
+
const res = await fetch(url, { method: "PUT", headers, body });
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const text = await res.text().catch(() => "");
|
|
114
|
+
throw new Error(`${res.status} ${res.statusText}: ${text}`);
|
|
115
|
+
}
|
|
116
|
+
return res.json();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function cmdUploadSourcemaps(opts) {
|
|
120
|
+
if (!opts.release) { console.error("--release is required"); process.exit(2); }
|
|
121
|
+
if (!opts.apiKey) { console.error("--api-key is required"); process.exit(2); }
|
|
122
|
+
if (opts._.length === 0) { console.error("Need at least one path"); process.exit(2); }
|
|
123
|
+
|
|
124
|
+
const targets = [];
|
|
125
|
+
for (const root of opts._) {
|
|
126
|
+
if (!fs.existsSync(root)) {
|
|
127
|
+
console.error(`Path not found: ${root}`);
|
|
128
|
+
process.exit(2);
|
|
129
|
+
}
|
|
130
|
+
for (const m of findMaps(root)) targets.push(m);
|
|
131
|
+
}
|
|
132
|
+
if (targets.length === 0) {
|
|
133
|
+
console.error("No .map files found");
|
|
134
|
+
process.exit(2);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let ok = 0, fail = 0, total = 0;
|
|
138
|
+
for (const m of targets) {
|
|
139
|
+
total++;
|
|
140
|
+
try {
|
|
141
|
+
const res = await uploadOne(opts, m);
|
|
142
|
+
ok++;
|
|
143
|
+
if (opts.verbose) console.log(` ✓ ${m} → ${res.id} ${res.created ? "(created)" : "(updated)"}`);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
fail++;
|
|
146
|
+
console.error(` ✗ ${m}: ${e.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
console.log(`uploaded ${ok}/${total} sourcemaps for release ${opts.release}` + (fail ? ` (${fail} failed)` : ""));
|
|
150
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function main() {
|
|
154
|
+
const argv = process.argv;
|
|
155
|
+
const cmd = argv[2];
|
|
156
|
+
if (!cmd || cmd === "-h" || cmd === "--help") { printHelp(); process.exit(0); }
|
|
157
|
+
|
|
158
|
+
const opts = parseArgs(argv);
|
|
159
|
+
opts.release ||= process.env.GOODLOGS_RELEASE || null;
|
|
160
|
+
opts.apiKey ||= process.env.GOODLOGS_API_KEY || null;
|
|
161
|
+
if (process.env.GOODLOGS_ENDPOINT) opts.endpoint = process.env.GOODLOGS_ENDPOINT;
|
|
162
|
+
|
|
163
|
+
// The first positional ends up as the subcommand; strip it.
|
|
164
|
+
if (opts._.length > 0 && opts._[0] === cmd) opts._.shift();
|
|
165
|
+
|
|
166
|
+
switch (cmd) {
|
|
167
|
+
case "upload-sourcemaps":
|
|
168
|
+
await cmdUploadSourcemaps(opts);
|
|
169
|
+
break;
|
|
170
|
+
default:
|
|
171
|
+
console.error(`Unknown command: ${cmd}`);
|
|
172
|
+
printHelp();
|
|
173
|
+
process.exit(2);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
main().catch((e) => {
|
|
178
|
+
console.error(e.stack || e.message || String(e));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
type Severity = "debug" | "info" | "warn" | "error" | "fatal";
|
|
2
|
+
interface GoodLogsOptions {
|
|
3
|
+
/** API key (gl_sk_... for logs, gl_pk_... for events only) */
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** API endpoint (default: auto-resolved from API key region) */
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
/** Flush interval in ms (default: 5000) */
|
|
8
|
+
flushInterval?: number;
|
|
9
|
+
/** Max batch size before auto-flush (default: 50) */
|
|
10
|
+
batchSize?: number;
|
|
11
|
+
/** Default context merged into every log/event */
|
|
12
|
+
defaultContext?: Record<string, unknown>;
|
|
13
|
+
/** Called on flush errors */
|
|
14
|
+
onError?: (error: Error) => void;
|
|
15
|
+
/** Disable SDK (useful for tests) */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Send SDK diagnostics to platform logs (default: true) */
|
|
18
|
+
telemetry?: boolean;
|
|
19
|
+
/** Auto-capture click events on elements with data-gl-event attribute (default: true in browser) */
|
|
20
|
+
autocapture?: boolean;
|
|
21
|
+
/** Use the unified /v1/envelope endpoint (NDJSON) instead of /v1/logs + /v1/events.
|
|
22
|
+
* Default: false (legacy). Opt-in during M0.1; will become the default once the
|
|
23
|
+
* legacy endpoints are retired. */
|
|
24
|
+
useEnvelope?: boolean;
|
|
25
|
+
/** Auto-instrument the browser `fetch` API.
|
|
26
|
+
* Default: true when `useEnvelope` is on and autocapture is not disabled.
|
|
27
|
+
* Each fetch call becomes a `op:http.client` span with http.url/method/status_code. */
|
|
28
|
+
autoFetch?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface LogEntry {
|
|
31
|
+
severity: Severity;
|
|
32
|
+
message: string;
|
|
33
|
+
properties?: Record<string, unknown>;
|
|
34
|
+
timestamp?: string;
|
|
35
|
+
}
|
|
36
|
+
interface EventEntry {
|
|
37
|
+
event: string;
|
|
38
|
+
distinctId?: string;
|
|
39
|
+
properties?: Record<string, unknown>;
|
|
40
|
+
timestamp?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Stack frame in OTel/Sentry-compatible shape (server matches this). */
|
|
43
|
+
interface ErrorFrame {
|
|
44
|
+
function?: string;
|
|
45
|
+
filename?: string;
|
|
46
|
+
lineno?: number;
|
|
47
|
+
colno?: number;
|
|
48
|
+
abs_path?: string;
|
|
49
|
+
module?: string;
|
|
50
|
+
in_app?: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface ErrorBreadcrumb {
|
|
53
|
+
ts?: string;
|
|
54
|
+
category?: string;
|
|
55
|
+
message?: string;
|
|
56
|
+
level?: string;
|
|
57
|
+
data?: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
interface ErrorEntry {
|
|
60
|
+
level?: "fatal" | "error" | "warning" | "info";
|
|
61
|
+
platform?: string;
|
|
62
|
+
exception_type?: string;
|
|
63
|
+
message: string;
|
|
64
|
+
frames?: ErrorFrame[];
|
|
65
|
+
breadcrumbs?: ErrorBreadcrumb[];
|
|
66
|
+
fingerprint?: string[];
|
|
67
|
+
tags?: Record<string, unknown>;
|
|
68
|
+
extra?: Record<string, unknown>;
|
|
69
|
+
user_id?: string;
|
|
70
|
+
release?: string;
|
|
71
|
+
environment?: string;
|
|
72
|
+
service?: string;
|
|
73
|
+
trace_id?: string;
|
|
74
|
+
span_id?: string;
|
|
75
|
+
timestamp?: string;
|
|
76
|
+
}
|
|
77
|
+
/** Options for `captureException`. */
|
|
78
|
+
interface CaptureContext {
|
|
79
|
+
tags?: Record<string, unknown>;
|
|
80
|
+
extra?: Record<string, unknown>;
|
|
81
|
+
fingerprint?: string[];
|
|
82
|
+
level?: "fatal" | "error" | "warning" | "info";
|
|
83
|
+
user_id?: string;
|
|
84
|
+
}
|
|
85
|
+
/** Options to startTransaction / startSpan. */
|
|
86
|
+
interface StartSpanOptions {
|
|
87
|
+
name?: string;
|
|
88
|
+
op?: string;
|
|
89
|
+
kind?: "internal" | "server" | "client" | "producer" | "consumer";
|
|
90
|
+
attributes?: Record<string, unknown>;
|
|
91
|
+
/** Parent span. When unset, this becomes a root span (a transaction). */
|
|
92
|
+
parent?: {
|
|
93
|
+
trace_id: string;
|
|
94
|
+
span_id: string;
|
|
95
|
+
};
|
|
96
|
+
/** Explicit trace id (e.g. for cross-service propagation via W3C traceparent). */
|
|
97
|
+
trace_id?: string;
|
|
98
|
+
}
|
|
99
|
+
/** Returned by startTransaction / startSpan; finish() emits the span. */
|
|
100
|
+
interface SpanHandle {
|
|
101
|
+
span_id: string;
|
|
102
|
+
trace_id: string;
|
|
103
|
+
setAttribute(key: string, value: unknown): void;
|
|
104
|
+
setStatus(status: "ok" | "error"): void;
|
|
105
|
+
addEvent(name: string, attributes?: Record<string, unknown>): void;
|
|
106
|
+
startChild(opts?: StartSpanOptions): SpanHandle;
|
|
107
|
+
finish(): void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
declare class GoodLogs {
|
|
111
|
+
private apiKey;
|
|
112
|
+
private endpoint;
|
|
113
|
+
private flushInterval;
|
|
114
|
+
private batchSize;
|
|
115
|
+
private defaultContext;
|
|
116
|
+
private onError;
|
|
117
|
+
private disabled;
|
|
118
|
+
private telemetry;
|
|
119
|
+
private useEnvelope;
|
|
120
|
+
private autoFetch;
|
|
121
|
+
private originalFetch;
|
|
122
|
+
/** Backup of XMLHttpRequest.prototype.{open,send} so detach() can restore. */
|
|
123
|
+
private xhrPatched;
|
|
124
|
+
private logBuffer;
|
|
125
|
+
private eventBuffer;
|
|
126
|
+
private errorBuffer;
|
|
127
|
+
private vitalBuffer;
|
|
128
|
+
private spanBuffer;
|
|
129
|
+
/** Rolling breadcrumb trail (last N), attached to each captured error. */
|
|
130
|
+
private breadcrumbBuffer;
|
|
131
|
+
private static readonly MAX_BREADCRUMBS;
|
|
132
|
+
private timer;
|
|
133
|
+
private visibilityHandler;
|
|
134
|
+
private clickHandler;
|
|
135
|
+
private lastPath;
|
|
136
|
+
/** Active op:navigation transaction; finished on next route change or shutdown. */
|
|
137
|
+
private navTransaction;
|
|
138
|
+
private errorHandler;
|
|
139
|
+
private rejectionHandler;
|
|
140
|
+
private anonymousId;
|
|
141
|
+
private sessionId;
|
|
142
|
+
constructor(options: GoodLogsOptions);
|
|
143
|
+
log(severity: Severity, message: string, properties?: Record<string, unknown>): void;
|
|
144
|
+
debug(message: string, properties?: Record<string, unknown>): void;
|
|
145
|
+
info(message: string, properties?: Record<string, unknown>): void;
|
|
146
|
+
warn(message: string, properties?: Record<string, unknown>): void;
|
|
147
|
+
error(message: string, properties?: Record<string, unknown>): void;
|
|
148
|
+
fatal(message: string, properties?: Record<string, unknown>): void;
|
|
149
|
+
/** Set the user ID for all subsequent events. Replaces the anonymous ID. */
|
|
150
|
+
identify(userId: string): void;
|
|
151
|
+
/** Get the current anonymous/user ID. */
|
|
152
|
+
getDistinctId(): string;
|
|
153
|
+
/** Reset identity — generates a new anonymous ID (e.g., on logout). */
|
|
154
|
+
reset(): void;
|
|
155
|
+
/** Get the current session ID. */
|
|
156
|
+
getSessionId(): string;
|
|
157
|
+
/** Start a new session (e.g., on navigation reset). */
|
|
158
|
+
newSession(): void;
|
|
159
|
+
/** Set a specific session ID. */
|
|
160
|
+
setSessionId(id: string): void;
|
|
161
|
+
track(event: string, properties?: Record<string, unknown> & {
|
|
162
|
+
distinctId?: string;
|
|
163
|
+
}): void;
|
|
164
|
+
/** Record a breadcrumb (timestamped trail event). Attached to subsequent errors. */
|
|
165
|
+
addBreadcrumb(crumb: ErrorBreadcrumb): void;
|
|
166
|
+
/** Capture an exception. Stack is parsed best-effort; falls back to a single frame. */
|
|
167
|
+
captureException(err: unknown, ctx?: CaptureContext): void;
|
|
168
|
+
/** Capture a string message as an issue. */
|
|
169
|
+
captureMessage(message: string, ctx?: CaptureContext): void;
|
|
170
|
+
private buildErrorEntry;
|
|
171
|
+
/** Send a Web Vitals sample. Routed as type:vital when useEnvelope=true;
|
|
172
|
+
* falls back to a tracked event in legacy mode. */
|
|
173
|
+
captureVital(metric: "lcp" | "inp" | "cls" | "fcp" | "ttfb" | "tbt" | "longtask", value_ms: number, extra?: {
|
|
174
|
+
route?: string;
|
|
175
|
+
rating?: "good" | "needs-improvement" | "poor";
|
|
176
|
+
attributes?: Record<string, unknown>;
|
|
177
|
+
}): void;
|
|
178
|
+
/** Start a new root span (transaction). */
|
|
179
|
+
startTransaction(opts?: StartSpanOptions): SpanHandle;
|
|
180
|
+
/** Start a child of `opts.parent`. If no parent is given a root span is
|
|
181
|
+
* created — equivalent to startTransaction. */
|
|
182
|
+
startSpan(opts?: StartSpanOptions): SpanHandle;
|
|
183
|
+
private makeSpan;
|
|
184
|
+
flush(): Promise<void>;
|
|
185
|
+
/** Flush remaining data and stop the timer */
|
|
186
|
+
shutdown(): Promise<void>;
|
|
187
|
+
private sendLogs;
|
|
188
|
+
private sendEvents;
|
|
189
|
+
/** Send a mixed batch through the unified /v1/envelope NDJSON endpoint. */
|
|
190
|
+
private sendEnvelope;
|
|
191
|
+
private postEnvelopeWithRetry;
|
|
192
|
+
private postWithRetry;
|
|
193
|
+
private startTimer;
|
|
194
|
+
private stopTimer;
|
|
195
|
+
/**
|
|
196
|
+
* In browsers: flush via fetch+keepalive when page goes hidden or closes.
|
|
197
|
+
* keepalive lets the request complete even after the page unloads,
|
|
198
|
+
* while still supporting custom headers (unlike sendBeacon).
|
|
199
|
+
*/
|
|
200
|
+
private attachPageLifecycle;
|
|
201
|
+
/** Auto-capture Core Web Vitals using PerformanceObserver */
|
|
202
|
+
private captureWebVitals;
|
|
203
|
+
/** Auto-capture page views on route changes (SPA-aware via History API) */
|
|
204
|
+
private attachPageviewCapture;
|
|
205
|
+
/** Start an op:navigation transaction for the given route. Finishes the previous one. */
|
|
206
|
+
private startNavTransaction;
|
|
207
|
+
/** Attach global window.onerror + unhandledrejection handlers. Browser only.
|
|
208
|
+
* Each uncaught error becomes a captureException(...) tagged with the
|
|
209
|
+
* active op:navigation transaction's trace_id/span_id. Idempotent — only
|
|
210
|
+
* attaches when no handlers were previously installed by this instance. */
|
|
211
|
+
private attachGlobalErrorHandlers;
|
|
212
|
+
private detachGlobalErrorHandlers;
|
|
213
|
+
/** Wrap globalThis.fetch so every call becomes a span. Idempotent. */
|
|
214
|
+
private attachFetchInstrumentation;
|
|
215
|
+
private detachFetchInstrumentation;
|
|
216
|
+
/** Wrap XMLHttpRequest so every call becomes a span. Idempotent.
|
|
217
|
+
* Patches the prototype globally — uses Symbol marker so multiple SDK
|
|
218
|
+
* instances or hot-reloads don't double-wrap. */
|
|
219
|
+
private attachXhrInstrumentation;
|
|
220
|
+
private detachXhrInstrumentation;
|
|
221
|
+
/** Auto-capture clicks on elements with text content or data-gl-event */
|
|
222
|
+
private attachClickCapture;
|
|
223
|
+
private detachClickCapture;
|
|
224
|
+
private detachPageLifecycle;
|
|
225
|
+
/** Fire-and-forget flush using fetch with keepalive — survives page unload */
|
|
226
|
+
private keepaliveFlush;
|
|
227
|
+
/** Fire-and-forget telemetry to platform logs. Never throws, never recurses. */
|
|
228
|
+
private sendTelemetry;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export { type CaptureContext as C, type ErrorBreadcrumb as E, GoodLogs as G, type LogEntry as L, type Severity as S, type ErrorEntry as a, type ErrorFrame as b, type EventEntry as c, type GoodLogsOptions as d };
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
type Severity = "debug" | "info" | "warn" | "error" | "fatal";
|
|
2
|
+
interface GoodLogsOptions {
|
|
3
|
+
/** API key (gl_sk_... for logs, gl_pk_... for events only) */
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** API endpoint (default: auto-resolved from API key region) */
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
/** Flush interval in ms (default: 5000) */
|
|
8
|
+
flushInterval?: number;
|
|
9
|
+
/** Max batch size before auto-flush (default: 50) */
|
|
10
|
+
batchSize?: number;
|
|
11
|
+
/** Default context merged into every log/event */
|
|
12
|
+
defaultContext?: Record<string, unknown>;
|
|
13
|
+
/** Called on flush errors */
|
|
14
|
+
onError?: (error: Error) => void;
|
|
15
|
+
/** Disable SDK (useful for tests) */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Send SDK diagnostics to platform logs (default: true) */
|
|
18
|
+
telemetry?: boolean;
|
|
19
|
+
/** Auto-capture click events on elements with data-gl-event attribute (default: true in browser) */
|
|
20
|
+
autocapture?: boolean;
|
|
21
|
+
/** Use the unified /v1/envelope endpoint (NDJSON) instead of /v1/logs + /v1/events.
|
|
22
|
+
* Default: false (legacy). Opt-in during M0.1; will become the default once the
|
|
23
|
+
* legacy endpoints are retired. */
|
|
24
|
+
useEnvelope?: boolean;
|
|
25
|
+
/** Auto-instrument the browser `fetch` API.
|
|
26
|
+
* Default: true when `useEnvelope` is on and autocapture is not disabled.
|
|
27
|
+
* Each fetch call becomes a `op:http.client` span with http.url/method/status_code. */
|
|
28
|
+
autoFetch?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface LogEntry {
|
|
31
|
+
severity: Severity;
|
|
32
|
+
message: string;
|
|
33
|
+
properties?: Record<string, unknown>;
|
|
34
|
+
timestamp?: string;
|
|
35
|
+
}
|
|
36
|
+
interface EventEntry {
|
|
37
|
+
event: string;
|
|
38
|
+
distinctId?: string;
|
|
39
|
+
properties?: Record<string, unknown>;
|
|
40
|
+
timestamp?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Stack frame in OTel/Sentry-compatible shape (server matches this). */
|
|
43
|
+
interface ErrorFrame {
|
|
44
|
+
function?: string;
|
|
45
|
+
filename?: string;
|
|
46
|
+
lineno?: number;
|
|
47
|
+
colno?: number;
|
|
48
|
+
abs_path?: string;
|
|
49
|
+
module?: string;
|
|
50
|
+
in_app?: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface ErrorBreadcrumb {
|
|
53
|
+
ts?: string;
|
|
54
|
+
category?: string;
|
|
55
|
+
message?: string;
|
|
56
|
+
level?: string;
|
|
57
|
+
data?: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
interface ErrorEntry {
|
|
60
|
+
level?: "fatal" | "error" | "warning" | "info";
|
|
61
|
+
platform?: string;
|
|
62
|
+
exception_type?: string;
|
|
63
|
+
message: string;
|
|
64
|
+
frames?: ErrorFrame[];
|
|
65
|
+
breadcrumbs?: ErrorBreadcrumb[];
|
|
66
|
+
fingerprint?: string[];
|
|
67
|
+
tags?: Record<string, unknown>;
|
|
68
|
+
extra?: Record<string, unknown>;
|
|
69
|
+
user_id?: string;
|
|
70
|
+
release?: string;
|
|
71
|
+
environment?: string;
|
|
72
|
+
service?: string;
|
|
73
|
+
trace_id?: string;
|
|
74
|
+
span_id?: string;
|
|
75
|
+
timestamp?: string;
|
|
76
|
+
}
|
|
77
|
+
/** Options for `captureException`. */
|
|
78
|
+
interface CaptureContext {
|
|
79
|
+
tags?: Record<string, unknown>;
|
|
80
|
+
extra?: Record<string, unknown>;
|
|
81
|
+
fingerprint?: string[];
|
|
82
|
+
level?: "fatal" | "error" | "warning" | "info";
|
|
83
|
+
user_id?: string;
|
|
84
|
+
}
|
|
85
|
+
/** Options to startTransaction / startSpan. */
|
|
86
|
+
interface StartSpanOptions {
|
|
87
|
+
name?: string;
|
|
88
|
+
op?: string;
|
|
89
|
+
kind?: "internal" | "server" | "client" | "producer" | "consumer";
|
|
90
|
+
attributes?: Record<string, unknown>;
|
|
91
|
+
/** Parent span. When unset, this becomes a root span (a transaction). */
|
|
92
|
+
parent?: {
|
|
93
|
+
trace_id: string;
|
|
94
|
+
span_id: string;
|
|
95
|
+
};
|
|
96
|
+
/** Explicit trace id (e.g. for cross-service propagation via W3C traceparent). */
|
|
97
|
+
trace_id?: string;
|
|
98
|
+
}
|
|
99
|
+
/** Returned by startTransaction / startSpan; finish() emits the span. */
|
|
100
|
+
interface SpanHandle {
|
|
101
|
+
span_id: string;
|
|
102
|
+
trace_id: string;
|
|
103
|
+
setAttribute(key: string, value: unknown): void;
|
|
104
|
+
setStatus(status: "ok" | "error"): void;
|
|
105
|
+
addEvent(name: string, attributes?: Record<string, unknown>): void;
|
|
106
|
+
startChild(opts?: StartSpanOptions): SpanHandle;
|
|
107
|
+
finish(): void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
declare class GoodLogs {
|
|
111
|
+
private apiKey;
|
|
112
|
+
private endpoint;
|
|
113
|
+
private flushInterval;
|
|
114
|
+
private batchSize;
|
|
115
|
+
private defaultContext;
|
|
116
|
+
private onError;
|
|
117
|
+
private disabled;
|
|
118
|
+
private telemetry;
|
|
119
|
+
private useEnvelope;
|
|
120
|
+
private autoFetch;
|
|
121
|
+
private originalFetch;
|
|
122
|
+
/** Backup of XMLHttpRequest.prototype.{open,send} so detach() can restore. */
|
|
123
|
+
private xhrPatched;
|
|
124
|
+
private logBuffer;
|
|
125
|
+
private eventBuffer;
|
|
126
|
+
private errorBuffer;
|
|
127
|
+
private vitalBuffer;
|
|
128
|
+
private spanBuffer;
|
|
129
|
+
/** Rolling breadcrumb trail (last N), attached to each captured error. */
|
|
130
|
+
private breadcrumbBuffer;
|
|
131
|
+
private static readonly MAX_BREADCRUMBS;
|
|
132
|
+
private timer;
|
|
133
|
+
private visibilityHandler;
|
|
134
|
+
private clickHandler;
|
|
135
|
+
private lastPath;
|
|
136
|
+
/** Active op:navigation transaction; finished on next route change or shutdown. */
|
|
137
|
+
private navTransaction;
|
|
138
|
+
private errorHandler;
|
|
139
|
+
private rejectionHandler;
|
|
140
|
+
private anonymousId;
|
|
141
|
+
private sessionId;
|
|
142
|
+
constructor(options: GoodLogsOptions);
|
|
143
|
+
log(severity: Severity, message: string, properties?: Record<string, unknown>): void;
|
|
144
|
+
debug(message: string, properties?: Record<string, unknown>): void;
|
|
145
|
+
info(message: string, properties?: Record<string, unknown>): void;
|
|
146
|
+
warn(message: string, properties?: Record<string, unknown>): void;
|
|
147
|
+
error(message: string, properties?: Record<string, unknown>): void;
|
|
148
|
+
fatal(message: string, properties?: Record<string, unknown>): void;
|
|
149
|
+
/** Set the user ID for all subsequent events. Replaces the anonymous ID. */
|
|
150
|
+
identify(userId: string): void;
|
|
151
|
+
/** Get the current anonymous/user ID. */
|
|
152
|
+
getDistinctId(): string;
|
|
153
|
+
/** Reset identity — generates a new anonymous ID (e.g., on logout). */
|
|
154
|
+
reset(): void;
|
|
155
|
+
/** Get the current session ID. */
|
|
156
|
+
getSessionId(): string;
|
|
157
|
+
/** Start a new session (e.g., on navigation reset). */
|
|
158
|
+
newSession(): void;
|
|
159
|
+
/** Set a specific session ID. */
|
|
160
|
+
setSessionId(id: string): void;
|
|
161
|
+
track(event: string, properties?: Record<string, unknown> & {
|
|
162
|
+
distinctId?: string;
|
|
163
|
+
}): void;
|
|
164
|
+
/** Record a breadcrumb (timestamped trail event). Attached to subsequent errors. */
|
|
165
|
+
addBreadcrumb(crumb: ErrorBreadcrumb): void;
|
|
166
|
+
/** Capture an exception. Stack is parsed best-effort; falls back to a single frame. */
|
|
167
|
+
captureException(err: unknown, ctx?: CaptureContext): void;
|
|
168
|
+
/** Capture a string message as an issue. */
|
|
169
|
+
captureMessage(message: string, ctx?: CaptureContext): void;
|
|
170
|
+
private buildErrorEntry;
|
|
171
|
+
/** Send a Web Vitals sample. Routed as type:vital when useEnvelope=true;
|
|
172
|
+
* falls back to a tracked event in legacy mode. */
|
|
173
|
+
captureVital(metric: "lcp" | "inp" | "cls" | "fcp" | "ttfb" | "tbt" | "longtask", value_ms: number, extra?: {
|
|
174
|
+
route?: string;
|
|
175
|
+
rating?: "good" | "needs-improvement" | "poor";
|
|
176
|
+
attributes?: Record<string, unknown>;
|
|
177
|
+
}): void;
|
|
178
|
+
/** Start a new root span (transaction). */
|
|
179
|
+
startTransaction(opts?: StartSpanOptions): SpanHandle;
|
|
180
|
+
/** Start a child of `opts.parent`. If no parent is given a root span is
|
|
181
|
+
* created — equivalent to startTransaction. */
|
|
182
|
+
startSpan(opts?: StartSpanOptions): SpanHandle;
|
|
183
|
+
private makeSpan;
|
|
184
|
+
flush(): Promise<void>;
|
|
185
|
+
/** Flush remaining data and stop the timer */
|
|
186
|
+
shutdown(): Promise<void>;
|
|
187
|
+
private sendLogs;
|
|
188
|
+
private sendEvents;
|
|
189
|
+
/** Send a mixed batch through the unified /v1/envelope NDJSON endpoint. */
|
|
190
|
+
private sendEnvelope;
|
|
191
|
+
private postEnvelopeWithRetry;
|
|
192
|
+
private postWithRetry;
|
|
193
|
+
private startTimer;
|
|
194
|
+
private stopTimer;
|
|
195
|
+
/**
|
|
196
|
+
* In browsers: flush via fetch+keepalive when page goes hidden or closes.
|
|
197
|
+
* keepalive lets the request complete even after the page unloads,
|
|
198
|
+
* while still supporting custom headers (unlike sendBeacon).
|
|
199
|
+
*/
|
|
200
|
+
private attachPageLifecycle;
|
|
201
|
+
/** Auto-capture Core Web Vitals using PerformanceObserver */
|
|
202
|
+
private captureWebVitals;
|
|
203
|
+
/** Auto-capture page views on route changes (SPA-aware via History API) */
|
|
204
|
+
private attachPageviewCapture;
|
|
205
|
+
/** Start an op:navigation transaction for the given route. Finishes the previous one. */
|
|
206
|
+
private startNavTransaction;
|
|
207
|
+
/** Attach global window.onerror + unhandledrejection handlers. Browser only.
|
|
208
|
+
* Each uncaught error becomes a captureException(...) tagged with the
|
|
209
|
+
* active op:navigation transaction's trace_id/span_id. Idempotent — only
|
|
210
|
+
* attaches when no handlers were previously installed by this instance. */
|
|
211
|
+
private attachGlobalErrorHandlers;
|
|
212
|
+
private detachGlobalErrorHandlers;
|
|
213
|
+
/** Wrap globalThis.fetch so every call becomes a span. Idempotent. */
|
|
214
|
+
private attachFetchInstrumentation;
|
|
215
|
+
private detachFetchInstrumentation;
|
|
216
|
+
/** Wrap XMLHttpRequest so every call becomes a span. Idempotent.
|
|
217
|
+
* Patches the prototype globally — uses Symbol marker so multiple SDK
|
|
218
|
+
* instances or hot-reloads don't double-wrap. */
|
|
219
|
+
private attachXhrInstrumentation;
|
|
220
|
+
private detachXhrInstrumentation;
|
|
221
|
+
/** Auto-capture clicks on elements with text content or data-gl-event */
|
|
222
|
+
private attachClickCapture;
|
|
223
|
+
private detachClickCapture;
|
|
224
|
+
private detachPageLifecycle;
|
|
225
|
+
/** Fire-and-forget flush using fetch with keepalive — survives page unload */
|
|
226
|
+
private keepaliveFlush;
|
|
227
|
+
/** Fire-and-forget telemetry to platform logs. Never throws, never recurses. */
|
|
228
|
+
private sendTelemetry;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export { type CaptureContext as C, type ErrorBreadcrumb as E, GoodLogs as G, type LogEntry as L, type Severity as S, type ErrorEntry as a, type ErrorFrame as b, type EventEntry as c, type GoodLogsOptions as d };
|