@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/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# @jovid1242/appready
|
|
2
|
+
|
|
3
|
+
Runtime error monitoring for apps built with AI.
|
|
4
|
+
|
|
5
|
+
When something breaks for a real user, AppReady catches it, groups it with the
|
|
6
|
+
identical ones, explains it in plain English, and gives you a prompt for the tool
|
|
7
|
+
you built with — Claude Code, Cursor, Lovable, Bolt, Codex, Replit or v0.
|
|
8
|
+
|
|
9
|
+
**3.1 KB gzipped. No dependencies. Never throws.**
|
|
10
|
+
|
|
11
|
+
> `0.x` while the API settles. It is small and stable in practice, but the
|
|
12
|
+
> version number is not making a promise it has not earned yet.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @jovid1242/appready
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { AppReady } from '@jovid1242/appready';
|
|
20
|
+
|
|
21
|
+
AppReady.init({
|
|
22
|
+
projectKey: 'pub_xxxxxxxx',
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That is the whole setup. Your project key is in AppReady under
|
|
27
|
+
**Project → Runtime Monitoring**.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What it catches
|
|
32
|
+
|
|
33
|
+
Automatically, from the moment `init` runs:
|
|
34
|
+
|
|
35
|
+
- uncaught exceptions
|
|
36
|
+
- unhandled promise rejections
|
|
37
|
+
- errors thrown during rendering
|
|
38
|
+
|
|
39
|
+
You can also report things yourself:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
AppReady.captureException(error);
|
|
43
|
+
AppReady.captureMessage('Checkout retried three times');
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Framework setup
|
|
49
|
+
|
|
50
|
+
### React
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// main.tsx
|
|
54
|
+
import { AppReady } from '@jovid1242/appready';
|
|
55
|
+
|
|
56
|
+
AppReady.init({ projectKey: 'pub_xxxxxxxx' });
|
|
57
|
+
|
|
58
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Next.js (App Router)
|
|
62
|
+
|
|
63
|
+
The SDK only runs in the browser, so initialise it from a client component
|
|
64
|
+
mounted in the root layout.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// app/appready.tsx
|
|
68
|
+
'use client';
|
|
69
|
+
import { useEffect } from 'react';
|
|
70
|
+
import { AppReady } from '@jovid1242/appready';
|
|
71
|
+
|
|
72
|
+
export function AppReadyInit() {
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
AppReady.init({ projectKey: 'pub_xxxxxxxx' });
|
|
75
|
+
}, []);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// app/layout.tsx
|
|
82
|
+
import { AppReadyInit } from './appready';
|
|
83
|
+
|
|
84
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
85
|
+
return (
|
|
86
|
+
<html lang="en">
|
|
87
|
+
<body>
|
|
88
|
+
<AppReadyInit />
|
|
89
|
+
{children}
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Vite
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// src/main.ts
|
|
100
|
+
import { AppReady } from '@jovid1242/appready';
|
|
101
|
+
|
|
102
|
+
AppReady.init({
|
|
103
|
+
projectKey: 'pub_xxxxxxxx',
|
|
104
|
+
// Errors are expected while you are building; only watch what you shipped.
|
|
105
|
+
enabled: import.meta.env.PROD,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Plain HTML, no build step
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<script
|
|
113
|
+
src="https://appready.tech/sdk/0.1.0/browser.min.js"
|
|
114
|
+
data-project-key="pub_xxxxxxxx"
|
|
115
|
+
defer
|
|
116
|
+
></script>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
It starts itself. To configure it by hand, leave off `data-project-key` and call
|
|
120
|
+
`AppReady.init({ ... })` after the script loads.
|
|
121
|
+
|
|
122
|
+
A pinned version is immutable and cached for a year — recommended, because a
|
|
123
|
+
third-party script on your site should not change without you deciding it
|
|
124
|
+
should. If you would rather receive patches automatically,
|
|
125
|
+
`https://appready.tech/sdk/v0/browser.min.js` follows the latest `0.x` release.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Options
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
AppReady.init({
|
|
133
|
+
projectKey: 'pub_xxxxxxxx', // required
|
|
134
|
+
environment: 'production', // default 'production'
|
|
135
|
+
release: 'v1.4.2', // your version or commit, if you have one
|
|
136
|
+
enabled: true, // set false to switch it off entirely
|
|
137
|
+
beforeSend: (event) => event, // last word on what gets sent; return null to drop
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Identifying a user (optional)
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
AppReady.setUser({ id: 'user_123' });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Takes an opaque id only. Anything that looks like an email address is refused
|
|
148
|
+
rather than stored — this SDK should not be how your user list reaches someone
|
|
149
|
+
else's servers.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Privacy
|
|
154
|
+
|
|
155
|
+
Sent with every event:
|
|
156
|
+
|
|
157
|
+
- error type, message and stack trace, **scrubbed** (see below)
|
|
158
|
+
- the page URL and a normalised route (`/orders/8412` → `/orders/:id`)
|
|
159
|
+
- browser, browser major version, operating system, viewport size
|
|
160
|
+
- your environment and release, if you set them
|
|
161
|
+
- the SDK version
|
|
162
|
+
|
|
163
|
+
**Never collected:** form values, passwords, cookies, `localStorage`,
|
|
164
|
+
`sessionStorage`, authorization headers, request bodies, keystrokes, or any kind
|
|
165
|
+
of session replay.
|
|
166
|
+
|
|
167
|
+
Before anything leaves the browser, messages and stack traces are scrubbed of
|
|
168
|
+
email addresses, bearer tokens, JWTs, Stripe/GitHub/AWS/Slack key shapes,
|
|
169
|
+
card-shaped numbers, and long values assigned to names like `apiKey` or
|
|
170
|
+
`password`. URLs lose credential-shaped query parameters. AppReady's servers
|
|
171
|
+
scrub everything again on arrival, because client-side scrubbing is a courtesy
|
|
172
|
+
to your users, not a security control.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## It will not break your app
|
|
177
|
+
|
|
178
|
+
This is a design constraint, not a hope:
|
|
179
|
+
|
|
180
|
+
- **Nothing throws.** Every entry point is wrapped. If the SDK is broken or
|
|
181
|
+
misconfigured, your app behaves exactly as if it were not installed.
|
|
182
|
+
- **Nothing blocks.** Events are batched and sent with `keepalive`, so a
|
|
183
|
+
navigation does not cancel them and nothing waits on the network.
|
|
184
|
+
- **Nothing amplifies.** An error thrown in a render loop is collapsed: the same
|
|
185
|
+
message inside a five-second window becomes one event with a count, and there
|
|
186
|
+
is a hard ceiling of 60 events per minute.
|
|
187
|
+
- **Nothing retries forever.** If AppReady is unreachable, events are dropped and
|
|
188
|
+
your app carries on. If the project key is rejected, the SDK stops trying.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Licence
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"use strict";(()=>{var v=["token","access_token","refresh_token","id_token","apikey","api_key","key","secret","password","passwd","pwd","auth","authorization","session","sid","signature","sig","credential","code"],S="[redacted]",_=[[/\b[\w.+-]+@[\w-]+\.[\w.-]+\b/g,"[email]"],[/\b(bearer|basic|token)\s+[A-Za-z0-9._~+/-]{12,}=*/gi,"$1 [redacted]"],[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,"[jwt]"],[/\bsk_(live|test)_[A-Za-z0-9]{8,}\b/g,"[stripe-key]"],[/\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{20,}\b/g,"[github-token]"],[/\bAKIA[0-9A-Z]{16}\b/g,"[aws-key]"],[/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,"[slack-token]"],[/\b(?:\d[ -]*?){13,19}\b/g,"[redacted-number]"]],O=/\b([\w.-]{2,40})\s*[=:]\s*["']?([A-Za-z0-9_\-+/]{24,})["']?/g;function a(e,t=2e3){if(!e)return"";let n=String(e).slice(0,t*2);for(let[r,i]of _)n=n.replace(r,i);return n=n.replace(O,(r,i)=>v.some(c=>i.toLowerCase().includes(c))?`${i}=${S}`:r),n.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g,"").slice(0,t)}function d(e){try{let t=new URL(e,"http://localhost");t.username="",t.password="",t.hash="";for(let n of Array.from(t.searchParams.keys())){let r=n.toLowerCase();v.some(i=>r===i||r.includes(i))&&t.searchParams.set(n,S)}return t.toString().slice(0,500)}catch{return String(e).split("?")[0].slice(0,500)}}function w(e,t=25){if(!e)return"";let n=String(e).split(`
|
|
2
|
+
`).slice(0,t+1);return a(n.map(r=>T(r)).join(`
|
|
3
|
+
`),4e3)}var T=e=>e.replace(/https?:\/\/[^\s)]+/g,t=>d(t));function E(){let t=I()?.userAgent??"",{name:n,version:r}=j(t);return{url:d(k()?.href??""),route:P(k()?.pathname??"/"),browser:n,browserVersion:r,os:z(t),viewport:M()}}function P(e){return e&&e.split("/").map(n=>n&&(/^\d+$/.test(n)?":id":/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(n)?":uuid":/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(n)||n.length>24&&!/[.\-_]/.test(n)?":id":n)).join("/").slice(0,200)||"/"}var I=()=>typeof navigator>"u"?void 0:navigator,k=()=>typeof location>"u"?void 0:location;function M(){if(typeof window>"u")return"";let e=window.innerWidth||0,t=window.innerHeight||0;return e&&t?`${e}x${t}`:""}function j(e){let t=[["Edge",/Edg(?:e|A|iOS)?\/([\d.]+)/],["Opera",/OPR\/([\d.]+)/],["Samsung Internet",/SamsungBrowser\/([\d.]+)/],["Firefox",/(?:Firefox|FxiOS)\/([\d.]+)/],["Chrome",/(?:Chrome|CriOS)\/([\d.]+)/],["Safari",/Version\/([\d.]+).*Safari/]];for(let[n,r]of t){let i=r.exec(e);if(i)return{name:n,version:(i[1]??"").split(".")[0]??""}}return{name:"Unknown",version:""}}function z(e){return/iPhone|iPad|iPod/.test(e)?"iOS":/Android/.test(e)?"Android":/Mac OS X|Macintosh/.test(e)?"macOS":/Windows/.test(e)?"Windows":/CrOS/.test(e)?"ChromeOS":/Linux/.test(e)?"Linux":"Unknown"}var u=class{constructor(t){this.options=t;this.queue=[];this.timer=null;this.dropped=0;this.disabled=!1;this.installUnloadFlush()}enqueue(t,n=!1){return this.disabled?!1:this.queue.length>=this.options.maxBatchSize*4?(this.dropped+=1,!1):(this.queue.push(t),n||this.queue.length>=this.options.maxBatchSize?(this.flush(),!0):(this.schedule(),!0))}schedule(){this.timer===null&&(this.timer=setTimeout(()=>{this.timer=null,this.flush()},this.options.flushIntervalMs),this.timer.unref?.())}flush(){if(this.timer!==null&&(clearTimeout(this.timer),this.timer=null),this.queue.length===0||this.disabled)return;let t=this.queue.splice(0,this.options.maxBatchSize),n=this.dropped;this.dropped=0,this.send({projectKey:this.options.projectKey,events:t,...n>0?{dropped:n}:{}})}async send(t){try{if(typeof fetch!="function")return;let n=typeof AbortController=="function"?new AbortController:null,r=n?setTimeout(()=>n.abort(),this.options.timeoutMs):null,i=await fetch(this.options.endpoint,{method:"POST",headers:{"content-type":"text/plain;charset=UTF-8"},body:JSON.stringify(t),keepalive:!0,mode:"cors",credentials:"omit",...n?{signal:n.signal}:{}});r&&clearTimeout(r),(i.status===401||i.status===403||i.status===404)&&(this.disabled=!0)}catch{}}installUnloadFlush(){try{if(typeof document>"u"||typeof addEventListener!="function")return;addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()},{capture:!0}),addEventListener("pagehide",()=>this.flush(),{capture:!0})}catch{}}};var C="0.1.0",K="https://appready.tech/api/runtime/v1/events",A=5e3,L=60,l=class{constructor(){this.transport=null;this.options={projectKey:"",environment:"production",enabled:!0};this.started=!1;this.seen=new Map;this.minuteBucket={startedAt:0,count:0}}init(t){try{if(this.started||!t?.projectKey||t.enabled===!1||typeof window>"u")return;this.options={environment:"production",enabled:!0,...t},this.transport=new u({endpoint:t.endpoint??K,projectKey:t.projectKey,flushIntervalMs:3e3,maxBatchSize:20,timeoutMs:8e3}),this.installHandlers(),this.started=!0,this.capture("integration_check","AppReadyIntegration","SDK initialised","",!0)}catch{}}captureException(t){try{let{name:n,message:r,stack:i}=p(t);this.capture("error",n,r,i,!0)}catch{}}captureMessage(t){try{this.capture("message","Message",String(t),"")}catch{}}setUser(t){try{if(!t?.id){this.userId=void 0;return}let n=String(t.id).slice(0,64);this.userId=/@|\s/.test(n)?void 0:n}catch{}}flush(){try{this.transport?.flush()}catch{}}capture(t,n,r,i,c=!1){if(!this.started&&t!=="integration_check"||!this.transport)return;let h=a(r,500),R=w(i),m=`${t}|${n}|${h}`,o=Date.now(),s=this.seen.get(m);if(s&&o-s.firstAt<A){s.count+=1;return}let g=s&&s.sent?s.count:1;if(this.seen.set(m,{firstAt:o,count:1,sent:!0}),this.pruneSeen(o),!this.withinRateLimit(o))return;let x=E(),y={kind:t,timestamp:new Date(o).toISOString(),errorType:a(n,120)||"Error",message:h,stack:R,sdkVersion:C,environment:this.options.environment??"production",...this.options.release?{release:String(this.options.release).slice(0,80)}:{},...this.userId?{userId:this.userId}:{},...g>1?{count:g}:{},...x},b=this.options.beforeSend?B(this.options.beforeSend,y):y;b&&this.transport.enqueue(b,c)}withinRateLimit(t){return t-this.minuteBucket.startedAt>6e4&&(this.minuteBucket={startedAt:t,count:0}),this.minuteBucket.count>=L?!1:(this.minuteBucket.count+=1,!0)}pruneSeen(t){if(!(this.seen.size<200)){for(let[n,r]of this.seen)t-r.firstAt>A*4&&this.seen.delete(n);this.seen.size>=500&&this.seen.clear()}}installHandlers(){try{addEventListener("error",t=>{if(!t.error&&t.target&&t.target!==window)return;let{name:n,message:r,stack:i}=p(t.error??t.message);this.capture("error",n,r,i,!0)},!0),addEventListener("unhandledrejection",t=>{let{name:n,message:r,stack:i}=p(t.reason);this.capture("unhandled_rejection",n,r,i,!0)})}catch{}}};function p(e){if(e instanceof Error)return{name:e.name||"Error",message:e.message||"",stack:e.stack??""};if(typeof e=="string")return{name:"Error",message:e,stack:""};if(e&&typeof e=="object"){let t=e;return{name:typeof t.name=="string"?t.name:"Error",message:typeof t.message=="string"?t.message:N(e),stack:typeof t.stack=="string"?t.stack:""}}return{name:"Error",message:String(e),stack:""}}function N(e){try{return JSON.stringify(e)?.slice(0,500)??String(e)}catch{return"[unserializable]"}}function B(e,t){try{return e(t)}catch{return t}}var f=new l;try{globalThis.AppReady=f;let e=typeof document<"u"&&document.currentScript||(typeof document<"u"?document.querySelector("script[data-project-key]"):null),t=e?.dataset?.projectKey;t&&f.init({projectKey:t,...e?.dataset?.environment?{environment:e.dataset.environment}:{},...e?.dataset?.release?{release:e.dataset.release}:{},...e?.dataset?.endpoint?{endpoint:e.dataset.endpoint}:{}})}catch{}})();
|
|
4
|
+
//# sourceMappingURL=browser.min.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/scrub.ts", "../src/context.ts", "../src/transport.ts", "../src/client.ts", "../src/cdn.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Removing things that must never leave the customer's browser.\n *\n * This runs in the SDK, and the backend runs its own pass over everything it\n * receives. That duplication is deliberate: anyone can POST to the ingestion\n * endpoint directly, so client-side scrubbing is a courtesy to the customer's\n * users, never a security control. Neither layer is allowed to assume the other\n * ran.\n *\n * The bias is towards over-removal. A redacted token in a stack trace costs\n * someone a little context; a real one costs them their account.\n */\n\n/** Query and body keys whose value is credential-shaped whatever they are called. */\nconst SENSITIVE_KEYS = [\n 'token',\n 'access_token',\n 'refresh_token',\n 'id_token',\n 'apikey',\n 'api_key',\n 'key',\n 'secret',\n 'password',\n 'passwd',\n 'pwd',\n 'auth',\n 'authorization',\n 'session',\n 'sid',\n 'signature',\n 'sig',\n 'credential',\n 'code',\n];\n\nconst REDACTED = '[redacted]';\n\n/** Patterns replaced anywhere they appear in free text. */\nconst PATTERNS: [RegExp, string][] = [\n // Email addresses. Common in \"user X not found\" messages.\n [/\\b[\\w.+-]+@[\\w-]+\\.[\\w.-]+\\b/g, '[email]'],\n // Bearer and similar header values that ended up in a message.\n [/\\b(bearer|basic|token)\\s+[A-Za-z0-9._~+/-]{12,}=*/gi, '$1 [redacted]'],\n // JWTs, which are three base64url segments and unmistakable.\n [/\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\b/g, '[jwt]'],\n // Known key shapes. Not exhaustive, and not meant to be \u2014 the generic\n // high-entropy rule below is the net underneath.\n [/\\bsk_(live|test)_[A-Za-z0-9]{8,}\\b/g, '[stripe-key]'],\n [/\\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{20,}\\b/g, '[github-token]'],\n [/\\bAKIA[0-9A-Z]{16}\\b/g, '[aws-key]'],\n [/\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/g, '[slack-token]'],\n // Card-shaped digit runs, spaced or not.\n [/\\b(?:\\d[ -]*?){13,19}\\b/g, '[redacted-number]'],\n];\n\n/**\n * A long run of high-entropy characters assigned to something.\n *\n * Deliberately conservative: it needs an assignment shape (`name=value` or\n * `name: value`) so ordinary long identifiers in a stack trace survive.\n */\nconst ASSIGNED_SECRET = /\\b([\\w.-]{2,40})\\s*[=:]\\s*[\"']?([A-Za-z0-9_\\-+/]{24,})[\"']?/g;\n\nexport function scrubText(input: string, maxLength = 2000): string {\n if (!input) return '';\n\n let out = String(input).slice(0, maxLength * 2);\n\n for (const [pattern, replacement] of PATTERNS) {\n out = out.replace(pattern, replacement);\n }\n\n out = out.replace(ASSIGNED_SECRET, (whole, name: string) =>\n SENSITIVE_KEYS.some((key) => name.toLowerCase().includes(key)) ? `${name}=${REDACTED}` : whole,\n );\n\n // Control characters would corrupt whatever renders this later.\n // Explicit escapes, not literal control bytes: those do not survive being\n // copied between files and silently turn this into a no-op.\n // eslint-disable-next-line no-control-regex\n return out.replace(/[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F]/g, '').slice(0, maxLength);\n}\n\n/**\n * Keeps a URL useful for grouping while removing anything credential-shaped.\n * The origin and path identify the page; the query rarely does and often carries\n * a session token.\n */\nexport function scrubUrl(raw: string): string {\n try {\n const url = new URL(raw, 'http://localhost');\n url.username = '';\n url.password = '';\n url.hash = '';\n\n for (const key of Array.from(url.searchParams.keys())) {\n const lower = key.toLowerCase();\n if (SENSITIVE_KEYS.some((sensitive) => lower === sensitive || lower.includes(sensitive))) {\n url.searchParams.set(key, REDACTED);\n }\n }\n return url.toString().slice(0, 500);\n } catch {\n return String(raw).split('?')[0]!.slice(0, 500);\n }\n}\n\n/**\n * A stack trace, scrubbed and bounded.\n *\n * Only the top frames matter for grouping and for a person reading it, and an\n * unbounded stack is how one event becomes a hundred kilobytes.\n */\nexport function scrubStack(stack: string | undefined, maxFrames = 25): string {\n if (!stack) return '';\n const lines = String(stack)\n .split('\\n')\n .slice(0, maxFrames + 1);\n return scrubText(lines.map((line) => scrubFrame(line)).join('\\n'), 4000);\n}\n\n/** Query strings inside a frame's file URL are as dangerous as anywhere else. */\nconst scrubFrame = (line: string): string =>\n line.replace(/https?:\\/\\/[^\\s)]+/g, (url) => scrubUrl(url));\n", "import { scrubUrl } from './scrub';\n\nexport interface RuntimeContext {\n url: string;\n route: string;\n browser: string;\n browserVersion: string;\n os: string;\n viewport: string;\n}\n\n/**\n * Just enough about the browser to make a report actionable.\n *\n * Parsed from the user agent rather than asked for, because every permission\n * prompt is a reason for the customer's users to distrust their app \u2014 and the\n * only questions this needs to answer are \"which browser\" and \"which page\".\n *\n * Deliberately absent: language, timezone, screen fingerprinting, IP-derived\n * location, anything that identifies a person rather than an environment.\n */\nexport function collectContext(): RuntimeContext {\n const nav = safeNavigator();\n const agent = nav?.userAgent ?? '';\n const { name, version } = parseBrowser(agent);\n\n return {\n url: scrubUrl(safeLocation()?.href ?? ''),\n route: routeOf(safeLocation()?.pathname ?? '/'),\n browser: name,\n browserVersion: version,\n os: parseOs(agent),\n viewport: viewport(),\n };\n}\n\n/**\n * A path with the parts that differ per visitor collapsed.\n *\n * `/orders/8412` and `/orders/9930` are the same page and the same bug, and a\n * dashboard that lists them separately is unusable for the one app that has a\n * thousand orders.\n */\nexport function routeOf(pathname: string): string {\n if (!pathname) return '/';\n const collapsed = pathname\n .split('/')\n .map((segment) => {\n if (!segment) return segment;\n if (/^\\d+$/.test(segment)) return ':id';\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {\n return ':uuid';\n }\n if (/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(segment)) return ':id';\n // A long opaque token in a path is an id of some kind.\n if (segment.length > 24 && !/[.\\-_]/.test(segment)) return ':id';\n return segment;\n })\n .join('/');\n return collapsed.slice(0, 200) || '/';\n}\n\nconst safeNavigator = (): Navigator | undefined =>\n typeof navigator === 'undefined' ? undefined : navigator;\n\nconst safeLocation = (): Location | undefined =>\n typeof location === 'undefined' ? undefined : location;\n\nfunction viewport(): string {\n if (typeof window === 'undefined') return '';\n const width = window.innerWidth || 0;\n const height = window.innerHeight || 0;\n return width && height ? `${width}x${height}` : '';\n}\n\n/**\n * Order matters. Every Chromium browser claims to be Chrome and Safari, and\n * Edge claims to be all three, so the most specific marker has to win.\n */\nfunction parseBrowser(agent: string): { name: string; version: string } {\n const checks: [string, RegExp][] = [\n ['Edge', /Edg(?:e|A|iOS)?\\/([\\d.]+)/],\n ['Opera', /OPR\\/([\\d.]+)/],\n ['Samsung Internet', /SamsungBrowser\\/([\\d.]+)/],\n ['Firefox', /(?:Firefox|FxiOS)\\/([\\d.]+)/],\n ['Chrome', /(?:Chrome|CriOS)\\/([\\d.]+)/],\n ['Safari', /Version\\/([\\d.]+).*Safari/],\n ];\n\n for (const [name, pattern] of checks) {\n const match = pattern.exec(agent);\n if (match) return { name, version: (match[1] ?? '').split('.')[0] ?? '' };\n }\n return { name: 'Unknown', version: '' };\n}\n\nfunction parseOs(agent: string): string {\n if (/iPhone|iPad|iPod/.test(agent)) return 'iOS';\n if (/Android/.test(agent)) return 'Android';\n if (/Mac OS X|Macintosh/.test(agent)) return 'macOS';\n if (/Windows/.test(agent)) return 'Windows';\n if (/CrOS/.test(agent)) return 'ChromeOS';\n if (/Linux/.test(agent)) return 'Linux';\n return 'Unknown';\n}\n", "import type { RuntimeEventPayload } from './types';\n\nexport interface TransportOptions {\n endpoint: string;\n projectKey: string;\n /** Events are held this long before a batch is sent. */\n flushIntervalMs: number;\n maxBatchSize: number;\n timeoutMs: number;\n}\n\n/**\n * Getting events out of the browser without getting in the way.\n *\n * Three rules, in priority order:\n *\n * 1. **Never break the host app.** Every path is wrapped, nothing rejects, and\n * a failed send is dropped rather than retried forever. A monitoring SDK\n * that takes down the app it monitors is worse than no monitoring.\n * 2. **Never block.** Sends are fire-and-forget with `keepalive`, so a\n * navigation mid-flush does not cancel them and does not delay the page.\n * 3. **Never amplify.** A page in an error loop can produce thousands of events\n * a second; the queue is bounded and the excess is dropped, counted, and\n * reported once rather than sent.\n */\nexport class Transport {\n private queue: RuntimeEventPayload[] = [];\n private timer: ReturnType<typeof setTimeout> | null = null;\n private dropped = 0;\n /** Set after a 4xx that will not improve \u2014 stop talking rather than hammer. */\n private disabled = false;\n\n constructor(private readonly options: TransportOptions) {\n this.installUnloadFlush();\n }\n\n /** Queues an event. Returns false when it was dropped. */\n enqueue(event: RuntimeEventPayload, immediate = false): boolean {\n if (this.disabled) return false;\n\n if (this.queue.length >= this.options.maxBatchSize * 4) {\n this.dropped += 1;\n return false;\n }\n\n this.queue.push(event);\n\n // A crash may be the last thing that happens before the page dies, so an\n // uncaught exception does not wait for the timer.\n if (immediate || this.queue.length >= this.options.maxBatchSize) {\n this.flush();\n return true;\n }\n\n this.schedule();\n return true;\n }\n\n private schedule(): void {\n if (this.timer !== null) return;\n this.timer = setTimeout(() => {\n this.timer = null;\n this.flush();\n }, this.options.flushIntervalMs);\n // Node typings give this an unref; browsers do not. Harmless either way.\n (this.timer as unknown as { unref?: () => void }).unref?.();\n }\n\n flush(): void {\n if (this.timer !== null) {\n clearTimeout(this.timer);\n this.timer = null;\n }\n if (this.queue.length === 0 || this.disabled) return;\n\n const batch = this.queue.splice(0, this.options.maxBatchSize);\n const dropped = this.dropped;\n this.dropped = 0;\n\n void this.send({\n projectKey: this.options.projectKey,\n events: batch,\n ...(dropped > 0 ? { dropped } : {}),\n });\n }\n\n private async send(body: unknown): Promise<void> {\n try {\n if (typeof fetch !== 'function') return;\n\n const controller = typeof AbortController === 'function' ? new AbortController() : null;\n const timer = controller\n ? setTimeout(() => controller.abort(), this.options.timeoutMs)\n : null;\n\n const response = await fetch(this.options.endpoint, {\n method: 'POST',\n // `text/plain` avoids a CORS preflight on every batch. The endpoint\n // parses the body itself; it never trusts the content type.\n headers: { 'content-type': 'text/plain;charset=UTF-8' },\n body: JSON.stringify(body),\n // Survives the page being closed mid-request, which is exactly when the\n // interesting errors happen.\n keepalive: true,\n mode: 'cors',\n // No cookies, ever. The endpoint answers `Allow-Origin: *`, and sending\n // credentials to a wildcard origin is both refused and wrong.\n credentials: 'omit',\n ...(controller ? { signal: controller.signal } : {}),\n });\n\n if (timer) clearTimeout(timer);\n\n // 401/403/404 mean the key is wrong or the project is gone. Retrying will\n // not fix that, and a broken key must not become a permanent load.\n if (response.status === 401 || response.status === 403 || response.status === 404) {\n this.disabled = true;\n }\n } catch {\n // Network down, blocked by an extension, CORS misconfigured, page closing.\n // All of them mean the same thing here: the customer's app carries on.\n }\n }\n\n /**\n * A last flush when the page goes away.\n *\n * `visibilitychange` rather than `unload`: mobile browsers frequently never\n * fire `unload`, and `pagehide` is not reliable on iOS either.\n */\n private installUnloadFlush(): void {\n try {\n if (typeof document === 'undefined' || typeof addEventListener !== 'function') return;\n addEventListener(\n 'visibilitychange',\n () => {\n if (document.visibilityState === 'hidden') this.flush();\n },\n { capture: true },\n );\n addEventListener('pagehide', () => this.flush(), { capture: true });\n } catch {\n // No document, or a hostile environment. Batching still works on a timer.\n }\n }\n}\n", "import { collectContext } from './context';\nimport { scrubStack, scrubText } from './scrub';\nimport { Transport } from './transport';\nimport type { AppReadyOptions, RuntimeEventKind, RuntimeEventPayload } from './types';\n\nexport const SDK_VERSION = '0.1.0';\n\nconst DEFAULT_ENDPOINT = 'https://appready.tech/api/runtime/v1/events';\n/** Identical errors inside this window collapse into one event with a count. */\nconst DEDUPE_WINDOW_MS = 5_000;\n/** Ceiling on events sent per minute, whatever the app does. */\nconst MAX_EVENTS_PER_MINUTE = 60;\n\ninterface Seen {\n firstAt: number;\n count: number;\n sent: boolean;\n}\n\n/**\n * The SDK.\n *\n * One rule governs every method here: **nothing this file does may be visible to\n * the customer's users.** No exception escapes, no promise rejects, no console\n * output, no measurable delay. If AppReady is broken, misconfigured or\n * unreachable, the app it is watching must behave exactly as if the SDK were not\n * installed. That is why almost every method has a try/catch that swallows \u2014\n * which would be wrong anywhere else in this codebase and is right here.\n */\nclass AppReadyClient {\n private transport: Transport | null = null;\n private options: Required<Pick<AppReadyOptions, 'environment' | 'enabled'>> & AppReadyOptions = {\n projectKey: '',\n environment: 'production',\n enabled: true,\n };\n\n private started = false;\n private userId: string | undefined;\n private readonly seen = new Map<string, Seen>();\n private minuteBucket = { startedAt: 0, count: 0 };\n\n init(options: AppReadyOptions): void {\n try {\n if (this.started) return; // Initialising twice must not double every event.\n if (!options?.projectKey) return;\n if (options.enabled === false) return;\n if (typeof window === 'undefined') return; // SSR: nothing to watch here.\n\n this.options = { environment: 'production', enabled: true, ...options };\n this.transport = new Transport({\n endpoint: options.endpoint ?? DEFAULT_ENDPOINT,\n projectKey: options.projectKey,\n flushIntervalMs: 3_000,\n maxBatchSize: 20,\n timeoutMs: 8_000,\n });\n\n this.installHandlers();\n this.started = true;\n\n // Tells the dashboard the integration is live without inventing an error\n // in the customer's production app. Sent immediately rather than batched:\n // it is exactly one event, and someone is watching a \"Verify connection\"\n // button while it happens.\n this.capture('integration_check', 'AppReadyIntegration', 'SDK initialised', '', true);\n } catch {\n // A failed init means no monitoring. It must never mean a failed app.\n }\n }\n\n captureException(error: unknown): void {\n try {\n const { name, message, stack } = describe(error);\n this.capture('error', name, message, stack, true);\n } catch {\n /* never throws */\n }\n }\n\n captureMessage(message: string): void {\n try {\n this.capture('message', 'Message', String(message), '');\n } catch {\n /* never throws */\n }\n }\n\n /**\n * Associates events with one of the customer's users.\n *\n * Optional, and takes an opaque id only. Anything that looks like an email is\n * refused rather than scrubbed later \u2014 the SDK should not be the reason a\n * customer's user list ends up on our servers.\n */\n setUser(user: { id: string } | null): void {\n try {\n if (!user?.id) {\n this.userId = undefined;\n return;\n }\n const id = String(user.id).slice(0, 64);\n this.userId = /@|\\s/.test(id) ? undefined : id;\n } catch {\n /* never throws */\n }\n }\n\n /** Sends anything queued. Useful right before a deliberate navigation. */\n flush(): void {\n try {\n this.transport?.flush();\n } catch {\n /* never throws */\n }\n }\n\n private capture(\n kind: RuntimeEventKind,\n errorType: string,\n message: string,\n stack: string,\n immediate = false,\n ): void {\n if (!this.started && kind !== 'integration_check') return;\n if (!this.transport) return;\n\n const cleanMessage = scrubText(message, 500);\n const cleanStack = scrubStack(stack);\n\n // A render loop can throw the same error thousands of times a second.\n // Collapsing them here \u2014 before the queue, before the network \u2014 is what\n // stops the SDK becoming the customer's biggest performance problem.\n const key = `${kind}|${errorType}|${cleanMessage}`;\n const now = Date.now();\n const previous = this.seen.get(key);\n\n if (previous && now - previous.firstAt < DEDUPE_WINDOW_MS) {\n previous.count += 1;\n return;\n }\n\n const carriedCount = previous && previous.sent ? previous.count : 1;\n this.seen.set(key, { firstAt: now, count: 1, sent: true });\n this.pruneSeen(now);\n\n if (!this.withinRateLimit(now)) return;\n\n const context = collectContext();\n const event: RuntimeEventPayload = {\n kind,\n timestamp: new Date(now).toISOString(),\n errorType: scrubText(errorType, 120) || 'Error',\n message: cleanMessage,\n stack: cleanStack,\n sdkVersion: SDK_VERSION,\n environment: this.options.environment ?? 'production',\n ...(this.options.release ? { release: String(this.options.release).slice(0, 80) } : {}),\n ...(this.userId ? { userId: this.userId } : {}),\n ...(carriedCount > 1 ? { count: carriedCount } : {}),\n ...context,\n };\n\n const final = this.options.beforeSend ? safeBeforeSend(this.options.beforeSend, event) : event;\n if (!final) return;\n\n this.transport.enqueue(final, immediate);\n }\n\n /** A hard ceiling, independent of dedupe \u2014 different errors also loop. */\n private withinRateLimit(now: number): boolean {\n if (now - this.minuteBucket.startedAt > 60_000) {\n this.minuteBucket = { startedAt: now, count: 0 };\n }\n if (this.minuteBucket.count >= MAX_EVENTS_PER_MINUTE) return false;\n this.minuteBucket.count += 1;\n return true;\n }\n\n /** The dedupe map is unbounded otherwise, and this runs in someone's tab. */\n private pruneSeen(now: number): void {\n if (this.seen.size < 200) return;\n for (const [key, entry] of this.seen) {\n if (now - entry.firstAt > DEDUPE_WINDOW_MS * 4) this.seen.delete(key);\n }\n // Still large: a pathological app producing unique messages forever.\n if (this.seen.size >= 500) this.seen.clear();\n }\n\n private installHandlers(): void {\n try {\n addEventListener(\n 'error',\n (event: ErrorEvent) => {\n // A failed <img>/<script> fires an error event with no `error`.\n if (!event.error && event.target && event.target !== window) return;\n const { name, message, stack } = describe(event.error ?? event.message);\n this.capture('error', name, message, stack, true);\n },\n // Capture phase, so an app's own handler cannot swallow it first.\n true,\n );\n\n addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {\n const { name, message, stack } = describe(event.reason);\n this.capture('unhandled_rejection', name, message, stack, true);\n });\n } catch {\n // No addEventListener: nothing to install, and nothing to break.\n }\n }\n}\n\n/** A thrown value can be anything at all, including a string or undefined. */\nfunction describe(value: unknown): { name: string; message: string; stack: string } {\n if (value instanceof Error) {\n return { name: value.name || 'Error', message: value.message || '', stack: value.stack ?? '' };\n }\n if (typeof value === 'string') return { name: 'Error', message: value, stack: '' };\n if (value && typeof value === 'object') {\n const record = value as { name?: unknown; message?: unknown; stack?: unknown };\n return {\n name: typeof record.name === 'string' ? record.name : 'Error',\n message: typeof record.message === 'string' ? record.message : safeStringify(value),\n stack: typeof record.stack === 'string' ? record.stack : '',\n };\n }\n return { name: 'Error', message: String(value), stack: '' };\n}\n\nfunction safeStringify(value: unknown): string {\n try {\n return JSON.stringify(value)?.slice(0, 500) ?? String(value);\n } catch {\n return '[unserializable]';\n }\n}\n\n/** A customer's callback throwing must not stop their app or lose the event. */\nfunction safeBeforeSend(\n hook: NonNullable<AppReadyOptions['beforeSend']>,\n event: RuntimeEventPayload,\n): RuntimeEventPayload | null {\n try {\n return hook(event);\n } catch {\n return event;\n }\n}\n\nexport const AppReady = new AppReadyClient();\nexport type { AppReadyOptions, RuntimeEventPayload };\n", "import { AppReady } from './client';\n\n/**\n * The CDN entry point.\n *\n * Same core as the npm package \u2014 this file only adds the auto-init that reads\n * the script tag. Two implementations of \"capture an error\" would mean the one\n * most people use is the one least tested.\n *\n * <script src=\"https://appready.tech/sdk/0.1.0/browser.min.js\"\n * data-project-key=\"pub_xxx\" defer></script>\n *\n * Without `data-project-key` nothing starts, and `AppReady.init(...)` can be\n * called by hand instead.\n */\ndeclare const globalThis: { AppReady?: unknown } & Record<string, unknown>;\n\ntry {\n globalThis.AppReady = AppReady;\n\n const script =\n (typeof document !== 'undefined' && (document.currentScript as HTMLScriptElement | null)) ||\n (typeof document !== 'undefined'\n ? document.querySelector<HTMLScriptElement>('script[data-project-key]')\n : null);\n\n const projectKey = script?.dataset?.projectKey;\n if (projectKey) {\n AppReady.init({\n projectKey,\n ...(script?.dataset?.environment ? { environment: script.dataset.environment } : {}),\n ...(script?.dataset?.release ? { release: script.dataset.release } : {}),\n ...(script?.dataset?.endpoint ? { endpoint: script.dataset.endpoint } : {}),\n });\n }\n} catch {\n // A monitoring script must never be the reason a page fails to load.\n}\n\nexport { AppReady };\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["SENSITIVE_KEYS", "REDACTED", "PATTERNS", "ASSIGNED_SECRET", "scrubText", "input", "maxLength", "out", "pattern", "replacement", "whole", "name", "key", "scrubUrl", "raw", "url", "lower", "sensitive", "scrubStack", "stack", "maxFrames", "lines", "line", "scrubFrame", "collectContext", "agent", "safeNavigator", "name", "version", "parseBrowser", "scrubUrl", "safeLocation", "routeOf", "parseOs", "viewport", "pathname", "segment", "width", "height", "checks", "pattern", "match", "Transport", "options", "event", "immediate", "batch", "dropped", "body", "controller", "timer", "response", "SDK_VERSION", "DEFAULT_ENDPOINT", "DEDUPE_WINDOW_MS", "MAX_EVENTS_PER_MINUTE", "AppReadyClient", "options", "Transport", "error", "name", "message", "stack", "describe", "user", "id", "kind", "errorType", "immediate", "cleanMessage", "scrubText", "cleanStack", "scrubStack", "key", "now", "previous", "carriedCount", "context", "collectContext", "event", "final", "safeBeforeSend", "entry", "value", "record", "safeStringify", "hook", "AppReady", "AppReady", "script", "projectKey"]
|
|
7
|
+
}
|
package/dist/cdn.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdn.d.ts","sourceRoot":"","sources":["../src/cdn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAuCpC,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AppReadyOptions, RuntimeEventPayload } from './types';
|
|
2
|
+
export declare const SDK_VERSION = "0.1.0";
|
|
3
|
+
/**
|
|
4
|
+
* The SDK.
|
|
5
|
+
*
|
|
6
|
+
* One rule governs every method here: **nothing this file does may be visible to
|
|
7
|
+
* the customer's users.** No exception escapes, no promise rejects, no console
|
|
8
|
+
* output, no measurable delay. If AppReady is broken, misconfigured or
|
|
9
|
+
* unreachable, the app it is watching must behave exactly as if the SDK were not
|
|
10
|
+
* installed. That is why almost every method has a try/catch that swallows —
|
|
11
|
+
* which would be wrong anywhere else in this codebase and is right here.
|
|
12
|
+
*/
|
|
13
|
+
declare class AppReadyClient {
|
|
14
|
+
private transport;
|
|
15
|
+
private options;
|
|
16
|
+
private started;
|
|
17
|
+
private userId;
|
|
18
|
+
private readonly seen;
|
|
19
|
+
private minuteBucket;
|
|
20
|
+
init(options: AppReadyOptions): void;
|
|
21
|
+
captureException(error: unknown): void;
|
|
22
|
+
captureMessage(message: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Associates events with one of the customer's users.
|
|
25
|
+
*
|
|
26
|
+
* Optional, and takes an opaque id only. Anything that looks like an email is
|
|
27
|
+
* refused rather than scrubbed later — the SDK should not be the reason a
|
|
28
|
+
* customer's user list ends up on our servers.
|
|
29
|
+
*/
|
|
30
|
+
setUser(user: {
|
|
31
|
+
id: string;
|
|
32
|
+
} | null): void;
|
|
33
|
+
/** Sends anything queued. Useful right before a deliberate navigation. */
|
|
34
|
+
flush(): void;
|
|
35
|
+
private capture;
|
|
36
|
+
/** A hard ceiling, independent of dedupe — different errors also loop. */
|
|
37
|
+
private withinRateLimit;
|
|
38
|
+
/** The dedupe map is unbounded otherwise, and this runs in someone's tab. */
|
|
39
|
+
private pruneSeen;
|
|
40
|
+
private installHandlers;
|
|
41
|
+
}
|
|
42
|
+
export declare const AppReady: AppReadyClient;
|
|
43
|
+
export type { AppReadyOptions, RuntimeEventPayload };
|
|
44
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAoB,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEtF,eAAO,MAAM,WAAW,UAAU,CAAC;AAcnC;;;;;;;;;GASG;AACH,cAAM,cAAc;IAClB,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,OAAO,CAIb;IAEF,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA2B;IAChD,OAAO,CAAC,YAAY,CAA8B;IAElD,IAAI,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IA6BpC,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAStC,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQrC;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,GAAG,IAAI;IAa1C,0EAA0E;IAC1E,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,OAAO;IAoDf,0EAA0E;IAC1E,OAAO,CAAC,eAAe;IASvB,6EAA6E;IAC7E,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,eAAe;CAsBxB;AAuCD,eAAO,MAAM,QAAQ,gBAAuB,CAAC;AAC7C,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface RuntimeContext {
|
|
2
|
+
url: string;
|
|
3
|
+
route: string;
|
|
4
|
+
browser: string;
|
|
5
|
+
browserVersion: string;
|
|
6
|
+
os: string;
|
|
7
|
+
viewport: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Just enough about the browser to make a report actionable.
|
|
11
|
+
*
|
|
12
|
+
* Parsed from the user agent rather than asked for, because every permission
|
|
13
|
+
* prompt is a reason for the customer's users to distrust their app — and the
|
|
14
|
+
* only questions this needs to answer are "which browser" and "which page".
|
|
15
|
+
*
|
|
16
|
+
* Deliberately absent: language, timezone, screen fingerprinting, IP-derived
|
|
17
|
+
* location, anything that identifies a person rather than an environment.
|
|
18
|
+
*/
|
|
19
|
+
export declare function collectContext(): RuntimeContext;
|
|
20
|
+
/**
|
|
21
|
+
* A path with the parts that differ per visitor collapsed.
|
|
22
|
+
*
|
|
23
|
+
* `/orders/8412` and `/orders/9930` are the same page and the same bug, and a
|
|
24
|
+
* dashboard that lists them separately is unusable for the one app that has a
|
|
25
|
+
* thousand orders.
|
|
26
|
+
*/
|
|
27
|
+
export declare function routeOf(pathname: string): string;
|
|
28
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,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;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,IAAI,cAAc,CAa/C;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiBhD"}
|