@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/dist/index.cjs ADDED
@@ -0,0 +1,438 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AppReady: () => AppReady,
24
+ SDK_VERSION: () => SDK_VERSION
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/scrub.ts
29
+ var SENSITIVE_KEYS = [
30
+ "token",
31
+ "access_token",
32
+ "refresh_token",
33
+ "id_token",
34
+ "apikey",
35
+ "api_key",
36
+ "key",
37
+ "secret",
38
+ "password",
39
+ "passwd",
40
+ "pwd",
41
+ "auth",
42
+ "authorization",
43
+ "session",
44
+ "sid",
45
+ "signature",
46
+ "sig",
47
+ "credential",
48
+ "code"
49
+ ];
50
+ var REDACTED = "[redacted]";
51
+ var PATTERNS = [
52
+ // Email addresses. Common in "user X not found" messages.
53
+ [/\b[\w.+-]+@[\w-]+\.[\w.-]+\b/g, "[email]"],
54
+ // Bearer and similar header values that ended up in a message.
55
+ [/\b(bearer|basic|token)\s+[A-Za-z0-9._~+/-]{12,}=*/gi, "$1 [redacted]"],
56
+ // JWTs, which are three base64url segments and unmistakable.
57
+ [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, "[jwt]"],
58
+ // Known key shapes. Not exhaustive, and not meant to be — the generic
59
+ // high-entropy rule below is the net underneath.
60
+ [/\bsk_(live|test)_[A-Za-z0-9]{8,}\b/g, "[stripe-key]"],
61
+ [/\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{20,}\b/g, "[github-token]"],
62
+ [/\bAKIA[0-9A-Z]{16}\b/g, "[aws-key]"],
63
+ [/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, "[slack-token]"],
64
+ // Card-shaped digit runs, spaced or not.
65
+ [/\b(?:\d[ -]*?){13,19}\b/g, "[redacted-number]"]
66
+ ];
67
+ var ASSIGNED_SECRET = /\b([\w.-]{2,40})\s*[=:]\s*["']?([A-Za-z0-9_\-+/]{24,})["']?/g;
68
+ function scrubText(input, maxLength = 2e3) {
69
+ if (!input) return "";
70
+ let out = String(input).slice(0, maxLength * 2);
71
+ for (const [pattern, replacement] of PATTERNS) {
72
+ out = out.replace(pattern, replacement);
73
+ }
74
+ out = out.replace(
75
+ ASSIGNED_SECRET,
76
+ (whole, name) => SENSITIVE_KEYS.some((key) => name.toLowerCase().includes(key)) ? `${name}=${REDACTED}` : whole
77
+ );
78
+ return out.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").slice(0, maxLength);
79
+ }
80
+ function scrubUrl(raw) {
81
+ try {
82
+ const url = new URL(raw, "http://localhost");
83
+ url.username = "";
84
+ url.password = "";
85
+ url.hash = "";
86
+ for (const key of Array.from(url.searchParams.keys())) {
87
+ const lower = key.toLowerCase();
88
+ if (SENSITIVE_KEYS.some((sensitive) => lower === sensitive || lower.includes(sensitive))) {
89
+ url.searchParams.set(key, REDACTED);
90
+ }
91
+ }
92
+ return url.toString().slice(0, 500);
93
+ } catch {
94
+ return String(raw).split("?")[0].slice(0, 500);
95
+ }
96
+ }
97
+ function scrubStack(stack, maxFrames = 25) {
98
+ if (!stack) return "";
99
+ const lines = String(stack).split("\n").slice(0, maxFrames + 1);
100
+ return scrubText(lines.map((line) => scrubFrame(line)).join("\n"), 4e3);
101
+ }
102
+ var scrubFrame = (line) => line.replace(/https?:\/\/[^\s)]+/g, (url) => scrubUrl(url));
103
+
104
+ // src/context.ts
105
+ function collectContext() {
106
+ const nav = safeNavigator();
107
+ const agent = nav?.userAgent ?? "";
108
+ const { name, version } = parseBrowser(agent);
109
+ return {
110
+ url: scrubUrl(safeLocation()?.href ?? ""),
111
+ route: routeOf(safeLocation()?.pathname ?? "/"),
112
+ browser: name,
113
+ browserVersion: version,
114
+ os: parseOs(agent),
115
+ viewport: viewport()
116
+ };
117
+ }
118
+ function routeOf(pathname) {
119
+ if (!pathname) return "/";
120
+ const collapsed = pathname.split("/").map((segment) => {
121
+ if (!segment) return segment;
122
+ if (/^\d+$/.test(segment)) return ":id";
123
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
124
+ return ":uuid";
125
+ }
126
+ if (/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(segment)) return ":id";
127
+ if (segment.length > 24 && !/[.\-_]/.test(segment)) return ":id";
128
+ return segment;
129
+ }).join("/");
130
+ return collapsed.slice(0, 200) || "/";
131
+ }
132
+ var safeNavigator = () => typeof navigator === "undefined" ? void 0 : navigator;
133
+ var safeLocation = () => typeof location === "undefined" ? void 0 : location;
134
+ function viewport() {
135
+ if (typeof window === "undefined") return "";
136
+ const width = window.innerWidth || 0;
137
+ const height = window.innerHeight || 0;
138
+ return width && height ? `${width}x${height}` : "";
139
+ }
140
+ function parseBrowser(agent) {
141
+ const checks = [
142
+ ["Edge", /Edg(?:e|A|iOS)?\/([\d.]+)/],
143
+ ["Opera", /OPR\/([\d.]+)/],
144
+ ["Samsung Internet", /SamsungBrowser\/([\d.]+)/],
145
+ ["Firefox", /(?:Firefox|FxiOS)\/([\d.]+)/],
146
+ ["Chrome", /(?:Chrome|CriOS)\/([\d.]+)/],
147
+ ["Safari", /Version\/([\d.]+).*Safari/]
148
+ ];
149
+ for (const [name, pattern] of checks) {
150
+ const match = pattern.exec(agent);
151
+ if (match) return { name, version: (match[1] ?? "").split(".")[0] ?? "" };
152
+ }
153
+ return { name: "Unknown", version: "" };
154
+ }
155
+ function parseOs(agent) {
156
+ if (/iPhone|iPad|iPod/.test(agent)) return "iOS";
157
+ if (/Android/.test(agent)) return "Android";
158
+ if (/Mac OS X|Macintosh/.test(agent)) return "macOS";
159
+ if (/Windows/.test(agent)) return "Windows";
160
+ if (/CrOS/.test(agent)) return "ChromeOS";
161
+ if (/Linux/.test(agent)) return "Linux";
162
+ return "Unknown";
163
+ }
164
+
165
+ // src/transport.ts
166
+ var Transport = class {
167
+ constructor(options) {
168
+ this.options = options;
169
+ this.queue = [];
170
+ this.timer = null;
171
+ this.dropped = 0;
172
+ /** Set after a 4xx that will not improve — stop talking rather than hammer. */
173
+ this.disabled = false;
174
+ this.installUnloadFlush();
175
+ }
176
+ /** Queues an event. Returns false when it was dropped. */
177
+ enqueue(event, immediate = false) {
178
+ if (this.disabled) return false;
179
+ if (this.queue.length >= this.options.maxBatchSize * 4) {
180
+ this.dropped += 1;
181
+ return false;
182
+ }
183
+ this.queue.push(event);
184
+ if (immediate || this.queue.length >= this.options.maxBatchSize) {
185
+ this.flush();
186
+ return true;
187
+ }
188
+ this.schedule();
189
+ return true;
190
+ }
191
+ schedule() {
192
+ if (this.timer !== null) return;
193
+ this.timer = setTimeout(() => {
194
+ this.timer = null;
195
+ this.flush();
196
+ }, this.options.flushIntervalMs);
197
+ this.timer.unref?.();
198
+ }
199
+ flush() {
200
+ if (this.timer !== null) {
201
+ clearTimeout(this.timer);
202
+ this.timer = null;
203
+ }
204
+ if (this.queue.length === 0 || this.disabled) return;
205
+ const batch = this.queue.splice(0, this.options.maxBatchSize);
206
+ const dropped = this.dropped;
207
+ this.dropped = 0;
208
+ void this.send({
209
+ projectKey: this.options.projectKey,
210
+ events: batch,
211
+ ...dropped > 0 ? { dropped } : {}
212
+ });
213
+ }
214
+ async send(body) {
215
+ try {
216
+ if (typeof fetch !== "function") return;
217
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
218
+ const timer = controller ? setTimeout(() => controller.abort(), this.options.timeoutMs) : null;
219
+ const response = await fetch(this.options.endpoint, {
220
+ method: "POST",
221
+ // `text/plain` avoids a CORS preflight on every batch. The endpoint
222
+ // parses the body itself; it never trusts the content type.
223
+ headers: { "content-type": "text/plain;charset=UTF-8" },
224
+ body: JSON.stringify(body),
225
+ // Survives the page being closed mid-request, which is exactly when the
226
+ // interesting errors happen.
227
+ keepalive: true,
228
+ mode: "cors",
229
+ // No cookies, ever. The endpoint answers `Allow-Origin: *`, and sending
230
+ // credentials to a wildcard origin is both refused and wrong.
231
+ credentials: "omit",
232
+ ...controller ? { signal: controller.signal } : {}
233
+ });
234
+ if (timer) clearTimeout(timer);
235
+ if (response.status === 401 || response.status === 403 || response.status === 404) {
236
+ this.disabled = true;
237
+ }
238
+ } catch {
239
+ }
240
+ }
241
+ /**
242
+ * A last flush when the page goes away.
243
+ *
244
+ * `visibilitychange` rather than `unload`: mobile browsers frequently never
245
+ * fire `unload`, and `pagehide` is not reliable on iOS either.
246
+ */
247
+ installUnloadFlush() {
248
+ try {
249
+ if (typeof document === "undefined" || typeof addEventListener !== "function") return;
250
+ addEventListener(
251
+ "visibilitychange",
252
+ () => {
253
+ if (document.visibilityState === "hidden") this.flush();
254
+ },
255
+ { capture: true }
256
+ );
257
+ addEventListener("pagehide", () => this.flush(), { capture: true });
258
+ } catch {
259
+ }
260
+ }
261
+ };
262
+
263
+ // src/client.ts
264
+ var SDK_VERSION = "0.1.0";
265
+ var DEFAULT_ENDPOINT = "https://appready.tech/api/runtime/v1/events";
266
+ var DEDUPE_WINDOW_MS = 5e3;
267
+ var MAX_EVENTS_PER_MINUTE = 60;
268
+ var AppReadyClient = class {
269
+ constructor() {
270
+ this.transport = null;
271
+ this.options = {
272
+ projectKey: "",
273
+ environment: "production",
274
+ enabled: true
275
+ };
276
+ this.started = false;
277
+ this.seen = /* @__PURE__ */ new Map();
278
+ this.minuteBucket = { startedAt: 0, count: 0 };
279
+ }
280
+ init(options) {
281
+ try {
282
+ if (this.started) return;
283
+ if (!options?.projectKey) return;
284
+ if (options.enabled === false) return;
285
+ if (typeof window === "undefined") return;
286
+ this.options = { environment: "production", enabled: true, ...options };
287
+ this.transport = new Transport({
288
+ endpoint: options.endpoint ?? DEFAULT_ENDPOINT,
289
+ projectKey: options.projectKey,
290
+ flushIntervalMs: 3e3,
291
+ maxBatchSize: 20,
292
+ timeoutMs: 8e3
293
+ });
294
+ this.installHandlers();
295
+ this.started = true;
296
+ this.capture("integration_check", "AppReadyIntegration", "SDK initialised", "", true);
297
+ } catch {
298
+ }
299
+ }
300
+ captureException(error) {
301
+ try {
302
+ const { name, message, stack } = describe(error);
303
+ this.capture("error", name, message, stack, true);
304
+ } catch {
305
+ }
306
+ }
307
+ captureMessage(message) {
308
+ try {
309
+ this.capture("message", "Message", String(message), "");
310
+ } catch {
311
+ }
312
+ }
313
+ /**
314
+ * Associates events with one of the customer's users.
315
+ *
316
+ * Optional, and takes an opaque id only. Anything that looks like an email is
317
+ * refused rather than scrubbed later — the SDK should not be the reason a
318
+ * customer's user list ends up on our servers.
319
+ */
320
+ setUser(user) {
321
+ try {
322
+ if (!user?.id) {
323
+ this.userId = void 0;
324
+ return;
325
+ }
326
+ const id = String(user.id).slice(0, 64);
327
+ this.userId = /@|\s/.test(id) ? void 0 : id;
328
+ } catch {
329
+ }
330
+ }
331
+ /** Sends anything queued. Useful right before a deliberate navigation. */
332
+ flush() {
333
+ try {
334
+ this.transport?.flush();
335
+ } catch {
336
+ }
337
+ }
338
+ capture(kind, errorType, message, stack, immediate = false) {
339
+ if (!this.started && kind !== "integration_check") return;
340
+ if (!this.transport) return;
341
+ const cleanMessage = scrubText(message, 500);
342
+ const cleanStack = scrubStack(stack);
343
+ const key = `${kind}|${errorType}|${cleanMessage}`;
344
+ const now = Date.now();
345
+ const previous = this.seen.get(key);
346
+ if (previous && now - previous.firstAt < DEDUPE_WINDOW_MS) {
347
+ previous.count += 1;
348
+ return;
349
+ }
350
+ const carriedCount = previous && previous.sent ? previous.count : 1;
351
+ this.seen.set(key, { firstAt: now, count: 1, sent: true });
352
+ this.pruneSeen(now);
353
+ if (!this.withinRateLimit(now)) return;
354
+ const context = collectContext();
355
+ const event = {
356
+ kind,
357
+ timestamp: new Date(now).toISOString(),
358
+ errorType: scrubText(errorType, 120) || "Error",
359
+ message: cleanMessage,
360
+ stack: cleanStack,
361
+ sdkVersion: SDK_VERSION,
362
+ environment: this.options.environment ?? "production",
363
+ ...this.options.release ? { release: String(this.options.release).slice(0, 80) } : {},
364
+ ...this.userId ? { userId: this.userId } : {},
365
+ ...carriedCount > 1 ? { count: carriedCount } : {},
366
+ ...context
367
+ };
368
+ const final = this.options.beforeSend ? safeBeforeSend(this.options.beforeSend, event) : event;
369
+ if (!final) return;
370
+ this.transport.enqueue(final, immediate);
371
+ }
372
+ /** A hard ceiling, independent of dedupe — different errors also loop. */
373
+ withinRateLimit(now) {
374
+ if (now - this.minuteBucket.startedAt > 6e4) {
375
+ this.minuteBucket = { startedAt: now, count: 0 };
376
+ }
377
+ if (this.minuteBucket.count >= MAX_EVENTS_PER_MINUTE) return false;
378
+ this.minuteBucket.count += 1;
379
+ return true;
380
+ }
381
+ /** The dedupe map is unbounded otherwise, and this runs in someone's tab. */
382
+ pruneSeen(now) {
383
+ if (this.seen.size < 200) return;
384
+ for (const [key, entry] of this.seen) {
385
+ if (now - entry.firstAt > DEDUPE_WINDOW_MS * 4) this.seen.delete(key);
386
+ }
387
+ if (this.seen.size >= 500) this.seen.clear();
388
+ }
389
+ installHandlers() {
390
+ try {
391
+ addEventListener(
392
+ "error",
393
+ (event) => {
394
+ if (!event.error && event.target && event.target !== window) return;
395
+ const { name, message, stack } = describe(event.error ?? event.message);
396
+ this.capture("error", name, message, stack, true);
397
+ },
398
+ // Capture phase, so an app's own handler cannot swallow it first.
399
+ true
400
+ );
401
+ addEventListener("unhandledrejection", (event) => {
402
+ const { name, message, stack } = describe(event.reason);
403
+ this.capture("unhandled_rejection", name, message, stack, true);
404
+ });
405
+ } catch {
406
+ }
407
+ }
408
+ };
409
+ function describe(value) {
410
+ if (value instanceof Error) {
411
+ return { name: value.name || "Error", message: value.message || "", stack: value.stack ?? "" };
412
+ }
413
+ if (typeof value === "string") return { name: "Error", message: value, stack: "" };
414
+ if (value && typeof value === "object") {
415
+ const record = value;
416
+ return {
417
+ name: typeof record.name === "string" ? record.name : "Error",
418
+ message: typeof record.message === "string" ? record.message : safeStringify(value),
419
+ stack: typeof record.stack === "string" ? record.stack : ""
420
+ };
421
+ }
422
+ return { name: "Error", message: String(value), stack: "" };
423
+ }
424
+ function safeStringify(value) {
425
+ try {
426
+ return JSON.stringify(value)?.slice(0, 500) ?? String(value);
427
+ } catch {
428
+ return "[unserializable]";
429
+ }
430
+ }
431
+ function safeBeforeSend(hook, event) {
432
+ try {
433
+ return hook(event);
434
+ } catch {
435
+ return event;
436
+ }
437
+ }
438
+ var AppReady = new AppReadyClient();
@@ -0,0 +1,3 @@
1
+ export { AppReady, SDK_VERSION } from './client';
2
+ export type { AppReadyOptions, RuntimeEventPayload, RuntimeEventKind } from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACjD,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC"}