@jovid1242/appready 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -0
- package/dist/browser.min.js +4 -0
- package/dist/browser.min.js.map +7 -0
- package/dist/cdn.d.ts +3 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/client.d.ts +44 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/context.d.ts +28 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/index.cjs +438 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +415 -0
- package/dist/scrub.d.ts +27 -0
- package/dist/scrub.d.ts.map +1 -0
- package/dist/transport.d.ts +45 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// src/scrub.ts
|
|
2
|
+
var SENSITIVE_KEYS = [
|
|
3
|
+
"token",
|
|
4
|
+
"access_token",
|
|
5
|
+
"refresh_token",
|
|
6
|
+
"id_token",
|
|
7
|
+
"apikey",
|
|
8
|
+
"api_key",
|
|
9
|
+
"key",
|
|
10
|
+
"secret",
|
|
11
|
+
"password",
|
|
12
|
+
"passwd",
|
|
13
|
+
"pwd",
|
|
14
|
+
"auth",
|
|
15
|
+
"authorization",
|
|
16
|
+
"session",
|
|
17
|
+
"sid",
|
|
18
|
+
"signature",
|
|
19
|
+
"sig",
|
|
20
|
+
"credential",
|
|
21
|
+
"code"
|
|
22
|
+
];
|
|
23
|
+
var REDACTED = "[redacted]";
|
|
24
|
+
var PATTERNS = [
|
|
25
|
+
// Email addresses. Common in "user X not found" messages.
|
|
26
|
+
[/\b[\w.+-]+@[\w-]+\.[\w.-]+\b/g, "[email]"],
|
|
27
|
+
// Bearer and similar header values that ended up in a message.
|
|
28
|
+
[/\b(bearer|basic|token)\s+[A-Za-z0-9._~+/-]{12,}=*/gi, "$1 [redacted]"],
|
|
29
|
+
// JWTs, which are three base64url segments and unmistakable.
|
|
30
|
+
[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, "[jwt]"],
|
|
31
|
+
// Known key shapes. Not exhaustive, and not meant to be — the generic
|
|
32
|
+
// high-entropy rule below is the net underneath.
|
|
33
|
+
[/\bsk_(live|test)_[A-Za-z0-9]{8,}\b/g, "[stripe-key]"],
|
|
34
|
+
[/\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{20,}\b/g, "[github-token]"],
|
|
35
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, "[aws-key]"],
|
|
36
|
+
[/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, "[slack-token]"],
|
|
37
|
+
// Card-shaped digit runs, spaced or not.
|
|
38
|
+
[/\b(?:\d[ -]*?){13,19}\b/g, "[redacted-number]"]
|
|
39
|
+
];
|
|
40
|
+
var ASSIGNED_SECRET = /\b([\w.-]{2,40})\s*[=:]\s*["']?([A-Za-z0-9_\-+/]{24,})["']?/g;
|
|
41
|
+
function scrubText(input, maxLength = 2e3) {
|
|
42
|
+
if (!input) return "";
|
|
43
|
+
let out = String(input).slice(0, maxLength * 2);
|
|
44
|
+
for (const [pattern, replacement] of PATTERNS) {
|
|
45
|
+
out = out.replace(pattern, replacement);
|
|
46
|
+
}
|
|
47
|
+
out = out.replace(
|
|
48
|
+
ASSIGNED_SECRET,
|
|
49
|
+
(whole, name) => SENSITIVE_KEYS.some((key) => name.toLowerCase().includes(key)) ? `${name}=${REDACTED}` : whole
|
|
50
|
+
);
|
|
51
|
+
return out.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").slice(0, maxLength);
|
|
52
|
+
}
|
|
53
|
+
function scrubUrl(raw) {
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(raw, "http://localhost");
|
|
56
|
+
url.username = "";
|
|
57
|
+
url.password = "";
|
|
58
|
+
url.hash = "";
|
|
59
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
60
|
+
const lower = key.toLowerCase();
|
|
61
|
+
if (SENSITIVE_KEYS.some((sensitive) => lower === sensitive || lower.includes(sensitive))) {
|
|
62
|
+
url.searchParams.set(key, REDACTED);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return url.toString().slice(0, 500);
|
|
66
|
+
} catch {
|
|
67
|
+
return String(raw).split("?")[0].slice(0, 500);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function scrubStack(stack, maxFrames = 25) {
|
|
71
|
+
if (!stack) return "";
|
|
72
|
+
const lines = String(stack).split("\n").slice(0, maxFrames + 1);
|
|
73
|
+
return scrubText(lines.map((line) => scrubFrame(line)).join("\n"), 4e3);
|
|
74
|
+
}
|
|
75
|
+
var scrubFrame = (line) => line.replace(/https?:\/\/[^\s)]+/g, (url) => scrubUrl(url));
|
|
76
|
+
|
|
77
|
+
// src/context.ts
|
|
78
|
+
function collectContext() {
|
|
79
|
+
const nav = safeNavigator();
|
|
80
|
+
const agent = nav?.userAgent ?? "";
|
|
81
|
+
const { name, version } = parseBrowser(agent);
|
|
82
|
+
return {
|
|
83
|
+
url: scrubUrl(safeLocation()?.href ?? ""),
|
|
84
|
+
route: routeOf(safeLocation()?.pathname ?? "/"),
|
|
85
|
+
browser: name,
|
|
86
|
+
browserVersion: version,
|
|
87
|
+
os: parseOs(agent),
|
|
88
|
+
viewport: viewport()
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function routeOf(pathname) {
|
|
92
|
+
if (!pathname) return "/";
|
|
93
|
+
const collapsed = pathname.split("/").map((segment) => {
|
|
94
|
+
if (!segment) return segment;
|
|
95
|
+
if (/^\d+$/.test(segment)) return ":id";
|
|
96
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
|
|
97
|
+
return ":uuid";
|
|
98
|
+
}
|
|
99
|
+
if (/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(segment)) return ":id";
|
|
100
|
+
if (segment.length > 24 && !/[.\-_]/.test(segment)) return ":id";
|
|
101
|
+
return segment;
|
|
102
|
+
}).join("/");
|
|
103
|
+
return collapsed.slice(0, 200) || "/";
|
|
104
|
+
}
|
|
105
|
+
var safeNavigator = () => typeof navigator === "undefined" ? void 0 : navigator;
|
|
106
|
+
var safeLocation = () => typeof location === "undefined" ? void 0 : location;
|
|
107
|
+
function viewport() {
|
|
108
|
+
if (typeof window === "undefined") return "";
|
|
109
|
+
const width = window.innerWidth || 0;
|
|
110
|
+
const height = window.innerHeight || 0;
|
|
111
|
+
return width && height ? `${width}x${height}` : "";
|
|
112
|
+
}
|
|
113
|
+
function parseBrowser(agent) {
|
|
114
|
+
const checks = [
|
|
115
|
+
["Edge", /Edg(?:e|A|iOS)?\/([\d.]+)/],
|
|
116
|
+
["Opera", /OPR\/([\d.]+)/],
|
|
117
|
+
["Samsung Internet", /SamsungBrowser\/([\d.]+)/],
|
|
118
|
+
["Firefox", /(?:Firefox|FxiOS)\/([\d.]+)/],
|
|
119
|
+
["Chrome", /(?:Chrome|CriOS)\/([\d.]+)/],
|
|
120
|
+
["Safari", /Version\/([\d.]+).*Safari/]
|
|
121
|
+
];
|
|
122
|
+
for (const [name, pattern] of checks) {
|
|
123
|
+
const match = pattern.exec(agent);
|
|
124
|
+
if (match) return { name, version: (match[1] ?? "").split(".")[0] ?? "" };
|
|
125
|
+
}
|
|
126
|
+
return { name: "Unknown", version: "" };
|
|
127
|
+
}
|
|
128
|
+
function parseOs(agent) {
|
|
129
|
+
if (/iPhone|iPad|iPod/.test(agent)) return "iOS";
|
|
130
|
+
if (/Android/.test(agent)) return "Android";
|
|
131
|
+
if (/Mac OS X|Macintosh/.test(agent)) return "macOS";
|
|
132
|
+
if (/Windows/.test(agent)) return "Windows";
|
|
133
|
+
if (/CrOS/.test(agent)) return "ChromeOS";
|
|
134
|
+
if (/Linux/.test(agent)) return "Linux";
|
|
135
|
+
return "Unknown";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/transport.ts
|
|
139
|
+
var Transport = class {
|
|
140
|
+
constructor(options) {
|
|
141
|
+
this.options = options;
|
|
142
|
+
this.queue = [];
|
|
143
|
+
this.timer = null;
|
|
144
|
+
this.dropped = 0;
|
|
145
|
+
/** Set after a 4xx that will not improve — stop talking rather than hammer. */
|
|
146
|
+
this.disabled = false;
|
|
147
|
+
this.installUnloadFlush();
|
|
148
|
+
}
|
|
149
|
+
/** Queues an event. Returns false when it was dropped. */
|
|
150
|
+
enqueue(event, immediate = false) {
|
|
151
|
+
if (this.disabled) return false;
|
|
152
|
+
if (this.queue.length >= this.options.maxBatchSize * 4) {
|
|
153
|
+
this.dropped += 1;
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
this.queue.push(event);
|
|
157
|
+
if (immediate || this.queue.length >= this.options.maxBatchSize) {
|
|
158
|
+
this.flush();
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
this.schedule();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
schedule() {
|
|
165
|
+
if (this.timer !== null) return;
|
|
166
|
+
this.timer = setTimeout(() => {
|
|
167
|
+
this.timer = null;
|
|
168
|
+
this.flush();
|
|
169
|
+
}, this.options.flushIntervalMs);
|
|
170
|
+
this.timer.unref?.();
|
|
171
|
+
}
|
|
172
|
+
flush() {
|
|
173
|
+
if (this.timer !== null) {
|
|
174
|
+
clearTimeout(this.timer);
|
|
175
|
+
this.timer = null;
|
|
176
|
+
}
|
|
177
|
+
if (this.queue.length === 0 || this.disabled) return;
|
|
178
|
+
const batch = this.queue.splice(0, this.options.maxBatchSize);
|
|
179
|
+
const dropped = this.dropped;
|
|
180
|
+
this.dropped = 0;
|
|
181
|
+
void this.send({
|
|
182
|
+
projectKey: this.options.projectKey,
|
|
183
|
+
events: batch,
|
|
184
|
+
...dropped > 0 ? { dropped } : {}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async send(body) {
|
|
188
|
+
try {
|
|
189
|
+
if (typeof fetch !== "function") return;
|
|
190
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
191
|
+
const timer = controller ? setTimeout(() => controller.abort(), this.options.timeoutMs) : null;
|
|
192
|
+
const response = await fetch(this.options.endpoint, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
// `text/plain` avoids a CORS preflight on every batch. The endpoint
|
|
195
|
+
// parses the body itself; it never trusts the content type.
|
|
196
|
+
headers: { "content-type": "text/plain;charset=UTF-8" },
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
// Survives the page being closed mid-request, which is exactly when the
|
|
199
|
+
// interesting errors happen.
|
|
200
|
+
keepalive: true,
|
|
201
|
+
mode: "cors",
|
|
202
|
+
// No cookies, ever. The endpoint answers `Allow-Origin: *`, and sending
|
|
203
|
+
// credentials to a wildcard origin is both refused and wrong.
|
|
204
|
+
credentials: "omit",
|
|
205
|
+
...controller ? { signal: controller.signal } : {}
|
|
206
|
+
});
|
|
207
|
+
if (timer) clearTimeout(timer);
|
|
208
|
+
if (response.status === 401 || response.status === 403 || response.status === 404) {
|
|
209
|
+
this.disabled = true;
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A last flush when the page goes away.
|
|
216
|
+
*
|
|
217
|
+
* `visibilitychange` rather than `unload`: mobile browsers frequently never
|
|
218
|
+
* fire `unload`, and `pagehide` is not reliable on iOS either.
|
|
219
|
+
*/
|
|
220
|
+
installUnloadFlush() {
|
|
221
|
+
try {
|
|
222
|
+
if (typeof document === "undefined" || typeof addEventListener !== "function") return;
|
|
223
|
+
addEventListener(
|
|
224
|
+
"visibilitychange",
|
|
225
|
+
() => {
|
|
226
|
+
if (document.visibilityState === "hidden") this.flush();
|
|
227
|
+
},
|
|
228
|
+
{ capture: true }
|
|
229
|
+
);
|
|
230
|
+
addEventListener("pagehide", () => this.flush(), { capture: true });
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/client.ts
|
|
237
|
+
var SDK_VERSION = "0.1.0";
|
|
238
|
+
var DEFAULT_ENDPOINT = "https://appready.tech/api/runtime/v1/events";
|
|
239
|
+
var DEDUPE_WINDOW_MS = 5e3;
|
|
240
|
+
var MAX_EVENTS_PER_MINUTE = 60;
|
|
241
|
+
var AppReadyClient = class {
|
|
242
|
+
constructor() {
|
|
243
|
+
this.transport = null;
|
|
244
|
+
this.options = {
|
|
245
|
+
projectKey: "",
|
|
246
|
+
environment: "production",
|
|
247
|
+
enabled: true
|
|
248
|
+
};
|
|
249
|
+
this.started = false;
|
|
250
|
+
this.seen = /* @__PURE__ */ new Map();
|
|
251
|
+
this.minuteBucket = { startedAt: 0, count: 0 };
|
|
252
|
+
}
|
|
253
|
+
init(options) {
|
|
254
|
+
try {
|
|
255
|
+
if (this.started) return;
|
|
256
|
+
if (!options?.projectKey) return;
|
|
257
|
+
if (options.enabled === false) return;
|
|
258
|
+
if (typeof window === "undefined") return;
|
|
259
|
+
this.options = { environment: "production", enabled: true, ...options };
|
|
260
|
+
this.transport = new Transport({
|
|
261
|
+
endpoint: options.endpoint ?? DEFAULT_ENDPOINT,
|
|
262
|
+
projectKey: options.projectKey,
|
|
263
|
+
flushIntervalMs: 3e3,
|
|
264
|
+
maxBatchSize: 20,
|
|
265
|
+
timeoutMs: 8e3
|
|
266
|
+
});
|
|
267
|
+
this.installHandlers();
|
|
268
|
+
this.started = true;
|
|
269
|
+
this.capture("integration_check", "AppReadyIntegration", "SDK initialised", "", true);
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
captureException(error) {
|
|
274
|
+
try {
|
|
275
|
+
const { name, message, stack } = describe(error);
|
|
276
|
+
this.capture("error", name, message, stack, true);
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
captureMessage(message) {
|
|
281
|
+
try {
|
|
282
|
+
this.capture("message", "Message", String(message), "");
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Associates events with one of the customer's users.
|
|
288
|
+
*
|
|
289
|
+
* Optional, and takes an opaque id only. Anything that looks like an email is
|
|
290
|
+
* refused rather than scrubbed later — the SDK should not be the reason a
|
|
291
|
+
* customer's user list ends up on our servers.
|
|
292
|
+
*/
|
|
293
|
+
setUser(user) {
|
|
294
|
+
try {
|
|
295
|
+
if (!user?.id) {
|
|
296
|
+
this.userId = void 0;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const id = String(user.id).slice(0, 64);
|
|
300
|
+
this.userId = /@|\s/.test(id) ? void 0 : id;
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** Sends anything queued. Useful right before a deliberate navigation. */
|
|
305
|
+
flush() {
|
|
306
|
+
try {
|
|
307
|
+
this.transport?.flush();
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
capture(kind, errorType, message, stack, immediate = false) {
|
|
312
|
+
if (!this.started && kind !== "integration_check") return;
|
|
313
|
+
if (!this.transport) return;
|
|
314
|
+
const cleanMessage = scrubText(message, 500);
|
|
315
|
+
const cleanStack = scrubStack(stack);
|
|
316
|
+
const key = `${kind}|${errorType}|${cleanMessage}`;
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
const previous = this.seen.get(key);
|
|
319
|
+
if (previous && now - previous.firstAt < DEDUPE_WINDOW_MS) {
|
|
320
|
+
previous.count += 1;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const carriedCount = previous && previous.sent ? previous.count : 1;
|
|
324
|
+
this.seen.set(key, { firstAt: now, count: 1, sent: true });
|
|
325
|
+
this.pruneSeen(now);
|
|
326
|
+
if (!this.withinRateLimit(now)) return;
|
|
327
|
+
const context = collectContext();
|
|
328
|
+
const event = {
|
|
329
|
+
kind,
|
|
330
|
+
timestamp: new Date(now).toISOString(),
|
|
331
|
+
errorType: scrubText(errorType, 120) || "Error",
|
|
332
|
+
message: cleanMessage,
|
|
333
|
+
stack: cleanStack,
|
|
334
|
+
sdkVersion: SDK_VERSION,
|
|
335
|
+
environment: this.options.environment ?? "production",
|
|
336
|
+
...this.options.release ? { release: String(this.options.release).slice(0, 80) } : {},
|
|
337
|
+
...this.userId ? { userId: this.userId } : {},
|
|
338
|
+
...carriedCount > 1 ? { count: carriedCount } : {},
|
|
339
|
+
...context
|
|
340
|
+
};
|
|
341
|
+
const final = this.options.beforeSend ? safeBeforeSend(this.options.beforeSend, event) : event;
|
|
342
|
+
if (!final) return;
|
|
343
|
+
this.transport.enqueue(final, immediate);
|
|
344
|
+
}
|
|
345
|
+
/** A hard ceiling, independent of dedupe — different errors also loop. */
|
|
346
|
+
withinRateLimit(now) {
|
|
347
|
+
if (now - this.minuteBucket.startedAt > 6e4) {
|
|
348
|
+
this.minuteBucket = { startedAt: now, count: 0 };
|
|
349
|
+
}
|
|
350
|
+
if (this.minuteBucket.count >= MAX_EVENTS_PER_MINUTE) return false;
|
|
351
|
+
this.minuteBucket.count += 1;
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
/** The dedupe map is unbounded otherwise, and this runs in someone's tab. */
|
|
355
|
+
pruneSeen(now) {
|
|
356
|
+
if (this.seen.size < 200) return;
|
|
357
|
+
for (const [key, entry] of this.seen) {
|
|
358
|
+
if (now - entry.firstAt > DEDUPE_WINDOW_MS * 4) this.seen.delete(key);
|
|
359
|
+
}
|
|
360
|
+
if (this.seen.size >= 500) this.seen.clear();
|
|
361
|
+
}
|
|
362
|
+
installHandlers() {
|
|
363
|
+
try {
|
|
364
|
+
addEventListener(
|
|
365
|
+
"error",
|
|
366
|
+
(event) => {
|
|
367
|
+
if (!event.error && event.target && event.target !== window) return;
|
|
368
|
+
const { name, message, stack } = describe(event.error ?? event.message);
|
|
369
|
+
this.capture("error", name, message, stack, true);
|
|
370
|
+
},
|
|
371
|
+
// Capture phase, so an app's own handler cannot swallow it first.
|
|
372
|
+
true
|
|
373
|
+
);
|
|
374
|
+
addEventListener("unhandledrejection", (event) => {
|
|
375
|
+
const { name, message, stack } = describe(event.reason);
|
|
376
|
+
this.capture("unhandled_rejection", name, message, stack, true);
|
|
377
|
+
});
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
function describe(value) {
|
|
383
|
+
if (value instanceof Error) {
|
|
384
|
+
return { name: value.name || "Error", message: value.message || "", stack: value.stack ?? "" };
|
|
385
|
+
}
|
|
386
|
+
if (typeof value === "string") return { name: "Error", message: value, stack: "" };
|
|
387
|
+
if (value && typeof value === "object") {
|
|
388
|
+
const record = value;
|
|
389
|
+
return {
|
|
390
|
+
name: typeof record.name === "string" ? record.name : "Error",
|
|
391
|
+
message: typeof record.message === "string" ? record.message : safeStringify(value),
|
|
392
|
+
stack: typeof record.stack === "string" ? record.stack : ""
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
return { name: "Error", message: String(value), stack: "" };
|
|
396
|
+
}
|
|
397
|
+
function safeStringify(value) {
|
|
398
|
+
try {
|
|
399
|
+
return JSON.stringify(value)?.slice(0, 500) ?? String(value);
|
|
400
|
+
} catch {
|
|
401
|
+
return "[unserializable]";
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function safeBeforeSend(hook, event) {
|
|
405
|
+
try {
|
|
406
|
+
return hook(event);
|
|
407
|
+
} catch {
|
|
408
|
+
return event;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
var AppReady = new AppReadyClient();
|
|
412
|
+
export {
|
|
413
|
+
AppReady,
|
|
414
|
+
SDK_VERSION
|
|
415
|
+
};
|
package/dist/scrub.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removing things that must never leave the customer's browser.
|
|
3
|
+
*
|
|
4
|
+
* This runs in the SDK, and the backend runs its own pass over everything it
|
|
5
|
+
* receives. That duplication is deliberate: anyone can POST to the ingestion
|
|
6
|
+
* endpoint directly, so client-side scrubbing is a courtesy to the customer's
|
|
7
|
+
* users, never a security control. Neither layer is allowed to assume the other
|
|
8
|
+
* ran.
|
|
9
|
+
*
|
|
10
|
+
* The bias is towards over-removal. A redacted token in a stack trace costs
|
|
11
|
+
* someone a little context; a real one costs them their account.
|
|
12
|
+
*/
|
|
13
|
+
export declare function scrubText(input: string, maxLength?: number): string;
|
|
14
|
+
/**
|
|
15
|
+
* Keeps a URL useful for grouping while removing anything credential-shaped.
|
|
16
|
+
* The origin and path identify the page; the query rarely does and often carries
|
|
17
|
+
* a session token.
|
|
18
|
+
*/
|
|
19
|
+
export declare function scrubUrl(raw: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* A stack trace, scrubbed and bounded.
|
|
22
|
+
*
|
|
23
|
+
* Only the top frames matter for grouping and for a person reading it, and an
|
|
24
|
+
* unbounded stack is how one event becomes a hundred kilobytes.
|
|
25
|
+
*/
|
|
26
|
+
export declare function scrubStack(stack: string | undefined, maxFrames?: number): string;
|
|
27
|
+
//# sourceMappingURL=scrub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrub.d.ts","sourceRoot":"","sources":["../src/scrub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqDH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,MAAM,CAkBjE;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiB5C;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,SAAK,GAAG,MAAM,CAM5E"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { RuntimeEventPayload } from './types';
|
|
2
|
+
export interface TransportOptions {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
projectKey: string;
|
|
5
|
+
/** Events are held this long before a batch is sent. */
|
|
6
|
+
flushIntervalMs: number;
|
|
7
|
+
maxBatchSize: number;
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Getting events out of the browser without getting in the way.
|
|
12
|
+
*
|
|
13
|
+
* Three rules, in priority order:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Never break the host app.** Every path is wrapped, nothing rejects, and
|
|
16
|
+
* a failed send is dropped rather than retried forever. A monitoring SDK
|
|
17
|
+
* that takes down the app it monitors is worse than no monitoring.
|
|
18
|
+
* 2. **Never block.** Sends are fire-and-forget with `keepalive`, so a
|
|
19
|
+
* navigation mid-flush does not cancel them and does not delay the page.
|
|
20
|
+
* 3. **Never amplify.** A page in an error loop can produce thousands of events
|
|
21
|
+
* a second; the queue is bounded and the excess is dropped, counted, and
|
|
22
|
+
* reported once rather than sent.
|
|
23
|
+
*/
|
|
24
|
+
export declare class Transport {
|
|
25
|
+
private readonly options;
|
|
26
|
+
private queue;
|
|
27
|
+
private timer;
|
|
28
|
+
private dropped;
|
|
29
|
+
/** Set after a 4xx that will not improve — stop talking rather than hammer. */
|
|
30
|
+
private disabled;
|
|
31
|
+
constructor(options: TransportOptions);
|
|
32
|
+
/** Queues an event. Returns false when it was dropped. */
|
|
33
|
+
enqueue(event: RuntimeEventPayload, immediate?: boolean): boolean;
|
|
34
|
+
private schedule;
|
|
35
|
+
flush(): void;
|
|
36
|
+
private send;
|
|
37
|
+
/**
|
|
38
|
+
* A last flush when the page goes away.
|
|
39
|
+
*
|
|
40
|
+
* `visibilitychange` rather than `unload`: mobile browsers frequently never
|
|
41
|
+
* fire `unload`, and `pagehide` is not reliable on iOS either.
|
|
42
|
+
*/
|
|
43
|
+
private installUnloadFlush;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,SAAS;IAOR,OAAO,CAAC,QAAQ,CAAC,OAAO;IANpC,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,OAAO,CAAK;IACpB,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAS;gBAEI,OAAO,EAAE,gBAAgB;IAItD,0DAA0D;IAC1D,OAAO,CAAC,KAAK,EAAE,mBAAmB,EAAE,SAAS,UAAQ,GAAG,OAAO;IAqB/D,OAAO,CAAC,QAAQ;IAUhB,KAAK,IAAI,IAAI;YAkBC,IAAI;IAsClB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;CAe3B"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** The SDK's public options. Everything except `projectKey` has a sane default. */
|
|
2
|
+
export interface AppReadyOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The project's public key, `pub_...`.
|
|
5
|
+
*
|
|
6
|
+
* Public on purpose: it identifies a project so events can be attributed, and
|
|
7
|
+
* grants nothing else. It appears in the customer's HTML, and that is fine.
|
|
8
|
+
*/
|
|
9
|
+
projectKey: string;
|
|
10
|
+
/** Where events go. Overridden only for self-hosting or local development. */
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
/** Free-form label — `production`, `staging`. Defaults to `production`. */
|
|
13
|
+
environment?: string;
|
|
14
|
+
/** Version or commit of the customer's app, if they have one to give. */
|
|
15
|
+
release?: string;
|
|
16
|
+
/** Off by default in local development, where errors are expected. */
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Last word on whether an event is sent. Return null to drop it, or a modified
|
|
20
|
+
* event to redact something only the customer knows is sensitive.
|
|
21
|
+
*/
|
|
22
|
+
beforeSend?: (event: RuntimeEventPayload) => RuntimeEventPayload | null;
|
|
23
|
+
}
|
|
24
|
+
export type RuntimeEventKind = 'error' | 'unhandled_rejection' | 'message' | 'integration_check';
|
|
25
|
+
export interface RuntimeEventPayload {
|
|
26
|
+
kind: RuntimeEventKind;
|
|
27
|
+
/** ISO 8601, from the browser's clock. The server records its own too. */
|
|
28
|
+
timestamp: string;
|
|
29
|
+
errorType: string;
|
|
30
|
+
message: string;
|
|
31
|
+
stack: string;
|
|
32
|
+
url: string;
|
|
33
|
+
route: string;
|
|
34
|
+
browser: string;
|
|
35
|
+
browserVersion: string;
|
|
36
|
+
os: string;
|
|
37
|
+
viewport: string;
|
|
38
|
+
environment: string;
|
|
39
|
+
release?: string;
|
|
40
|
+
sdkVersion: string;
|
|
41
|
+
/** Only ever an opaque id the customer chose to set. Never a name or email. */
|
|
42
|
+
userId?: string;
|
|
43
|
+
/** How many identical events this one stands for, when a loop was collapsed. */
|
|
44
|
+
count?: number;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,mBAAmB,GAAG,IAAI,CAAC;CACzE;AAED,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,qBAAqB,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAEjG,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,gBAAgB,CAAC;IACvB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jovid1242/appready",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Runtime error monitoring for apps built with AI. Two lines to install, plain-English explanations, and a fix prompt for the tool you built with.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://appready.tech",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/jovid1242/appready.git",
|
|
10
|
+
"directory": "packages/browser-sdk"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"error-monitoring",
|
|
14
|
+
"runtime-errors",
|
|
15
|
+
"browser",
|
|
16
|
+
"appready",
|
|
17
|
+
"observability"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"require": "./dist/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "node build.mjs && node publish-cdn.mjs",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"prepublishOnly": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"esbuild": "^0.28.2",
|
|
43
|
+
"happy-dom": "^20.14.0",
|
|
44
|
+
"typescript": "^5.9.0",
|
|
45
|
+
"vitest": "^3.2.7"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
}
|
|
53
|
+
}
|