@allstak/react 0.3.0 → 0.3.2
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/CHANGELOG.md +57 -0
- package/README.md +211 -45
- package/dist/build/chunk-G6VPGDP6.mjs +214 -0
- package/dist/build/chunk-G6VPGDP6.mjs.map +1 -0
- package/dist/build/chunk-ZY4H5AN4.mjs +32 -0
- package/dist/build/chunk-ZY4H5AN4.mjs.map +1 -0
- package/dist/build/next.d.mts +33 -0
- package/dist/build/next.d.ts +33 -0
- package/dist/build/next.js +274 -0
- package/dist/build/next.js.map +1 -0
- package/dist/build/next.mjs +26 -0
- package/dist/build/next.mjs.map +1 -0
- package/dist/build/sourcemaps.d.mts +160 -0
- package/dist/build/sourcemaps.d.ts +160 -0
- package/dist/build/sourcemaps.js +248 -0
- package/dist/build/sourcemaps.js.map +1 -0
- package/dist/build/sourcemaps.mjs +23 -0
- package/dist/build/sourcemaps.mjs.map +1 -0
- package/dist/build/vite.d.mts +65 -0
- package/dist/build/vite.d.ts +65 -0
- package/dist/build/vite.js +269 -0
- package/dist/build/vite.js.map +1 -0
- package/dist/build/vite.mjs +43 -0
- package/dist/build/vite.mjs.map +1 -0
- package/dist/build/webpack.d.mts +58 -0
- package/dist/build/webpack.d.ts +58 -0
- package/dist/build/webpack.js +256 -0
- package/dist/build/webpack.js.map +1 -0
- package/dist/build/webpack.mjs +8 -0
- package/dist/build/webpack.mjs.map +1 -0
- package/dist/index.d.mts +196 -42
- package/dist/index.d.ts +196 -42
- package/dist/index.js +435 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +435 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +25 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@allstak/react` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.1] — 2026-05-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Version constant in `client.ts` now matches `package.json` (was `0.3.0`).
|
|
13
|
+
- Added version consistency test to CI.
|
|
14
|
+
|
|
15
|
+
## [0.3.0] — 2026-05-08
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Full automatic HTTP instrumentation (fetch and XHR interception).
|
|
20
|
+
- Source map upload pipeline with Vite, Webpack, and Next.js build plugins.
|
|
21
|
+
- Per-frame `debugId` resolution via `globalThis._allstakDebugIds`.
|
|
22
|
+
- `debugMeta.images[]` aggregation in error payloads.
|
|
23
|
+
|
|
24
|
+
## [0.2.0] — 2026-04-25
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Scope management for tags, extras, and context.
|
|
29
|
+
- Distributed tracing with automatic `traceparent` propagation.
|
|
30
|
+
- Web session replay (surrogate events).
|
|
31
|
+
- React Router integration helpers.
|
|
32
|
+
- Debug-ID based source map resolution.
|
|
33
|
+
|
|
34
|
+
## [0.1.4] — 2026-04-10
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Standalone release on public npm as `@allstak/react`.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `ErrorBoundary` component with `componentDidCatch` capture.
|
|
43
|
+
- Automatic `window.onerror` and `onunhandledrejection` handlers.
|
|
44
|
+
- `beforeSend` callback, `sampleRate`, `setTags`/`setExtra`/`setContext`.
|
|
45
|
+
- `flush()` with bounded timeout.
|
|
46
|
+
- Web Vitals (LCP, FID, CLS) capture.
|
|
47
|
+
|
|
48
|
+
## [0.1.1] — 2026-03-28
|
|
49
|
+
|
|
50
|
+
Initial professional release.
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- Core error capture with stack trace parsing.
|
|
55
|
+
- Breadcrumb ring buffer (max 50).
|
|
56
|
+
- Fail-open transport with exponential backoff.
|
|
57
|
+
- Circuit breaker on 401 responses.
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @allstak/react
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**One wrapper. Full error tracking + Web Vitals + breadcrumbs + HTTP. For React 17, 18, and 19.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@allstak/react)
|
|
6
6
|
[](https://github.com/AllStak/allstak-react/actions)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
|
-
Official AllStak SDK for React — error boundary
|
|
9
|
+
Official AllStak SDK for React — provider component, error boundary, hooks, and Web Vitals on top of a self-contained browser client.
|
|
10
10
|
|
|
11
11
|
## Dashboard
|
|
12
12
|
|
|
@@ -16,42 +16,139 @@ View captured events live at [app.allstak.sa](https://app.allstak.sa).
|
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
- `
|
|
22
|
-
-
|
|
23
|
-
-
|
|
19
|
+
- **One-wrapper setup** — `<AllStakProvider>` initializes everything
|
|
20
|
+
- React error boundary with custom fallback UI + `resetError`
|
|
21
|
+
- `window.onerror` + `unhandledrejection` capture
|
|
22
|
+
- `console.warn` + `console.error` capture by default; `log`/`info` opt-in
|
|
23
|
+
- `fetch` and `XMLHttpRequest` breadcrumbs (200 / 4xx / 5xx / network failure)
|
|
24
|
+
- Full HTTP request events with `enableHttpTracking: true` + privacy-first redaction
|
|
25
|
+
- Auto navigation breadcrumbs (history.pushState patch + popstate)
|
|
26
|
+
- Web Vitals via PerformanceObserver (CLS / LCP / INP / FCP / TTFB)
|
|
27
|
+
- React Router + Next.js helpers
|
|
28
|
+
- `useAllStak()` hook with stable identity
|
|
24
29
|
- Full TypeScript types
|
|
25
30
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
Once integrated, every event flows to your AllStak dashboard:
|
|
31
|
+
## Quick Start
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **HTTP** — outbound `fetch` timing, status codes, failed calls
|
|
33
|
-
- **Performance** — mount and update timing from the profiler HOC
|
|
34
|
-
- **Alerts** — email and webhook notifications on regressions
|
|
33
|
+
> **Time to first event: under 2 minutes.**
|
|
34
|
+
> Create a project at [app.allstak.sa](https://app.allstak.sa) to get your API key.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
### 1. Install
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
npm install @allstak/react
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
### 2. Wrap your app
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
```tsx
|
|
45
|
+
import { AllStakProvider } from '@allstak/react';
|
|
46
|
+
|
|
47
|
+
export function App() {
|
|
48
|
+
return (
|
|
49
|
+
<AllStakProvider
|
|
50
|
+
apiKey="ask_live_YOUR_KEY_HERE"
|
|
51
|
+
environment="production"
|
|
52
|
+
release="web@1.0.0"
|
|
53
|
+
debug
|
|
54
|
+
>
|
|
55
|
+
<AppRoot />
|
|
56
|
+
</AllStakProvider>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 3. Trigger a test error
|
|
45
62
|
|
|
46
63
|
```tsx
|
|
64
|
+
import { AllStak } from '@allstak/react';
|
|
65
|
+
|
|
66
|
+
// On a button press or on mount:
|
|
67
|
+
AllStak.captureException(new Error('test: hello from allstak-react'));
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. Check your dashboard
|
|
71
|
+
|
|
72
|
+
Open [app.allstak.sa](https://app.allstak.sa) — the error appears within seconds.
|
|
73
|
+
|
|
74
|
+
When `debug` is enabled you'll see `[AllStak] Initialized — session <id>` and `[AllStak] Navigation auto-instrumentation enabled` in your browser console.
|
|
75
|
+
|
|
76
|
+
`AllStakProvider` handles everything:
|
|
77
|
+
- Initializes the SDK
|
|
78
|
+
- Wraps your app in an `AllStakErrorBoundary`
|
|
79
|
+
- Hooks `window.onerror`, `unhandledrejection`, `console.*`, `fetch`, `history.*`
|
|
80
|
+
- Starts Web Vitals collection
|
|
81
|
+
- Tags every event with `platform: 'browser'`, sdk name + version
|
|
82
|
+
|
|
83
|
+
## Automatic Capture Matrix
|
|
84
|
+
|
|
85
|
+
Status legend:
|
|
86
|
+
- ✅ **Auto by default** — works the moment the provider mounts
|
|
87
|
+
- ⚙️ **Auto when enabled by config** — set the listed prop
|
|
88
|
+
- 🛠 **Manual only** — call the listed function yourself
|
|
89
|
+
- 🟡 **Implemented but not browser-verified end-to-end yet**
|
|
90
|
+
- ❌ **Not supported yet** (roadmap)
|
|
91
|
+
|
|
92
|
+
| Capability | Status | Config / API | Wire form |
|
|
93
|
+
|---|---|---|---|
|
|
94
|
+
| Render errors via `<AllStakProvider>` boundary | ✅ | always-on inside the provider | `POST /ingest/v1/errors` with `metadata.source = 'AllStakProvider.ErrorBoundary'` + `componentStack` |
|
|
95
|
+
| `window.onerror` global handler | ✅ | `autoCaptureBrowserErrors` (default `true`) | `POST /ingest/v1/errors` with `metadata.source = 'window.onerror'` |
|
|
96
|
+
| `window.unhandledrejection` | ✅ | `autoCaptureBrowserErrors` | `POST /ingest/v1/errors` with `metadata.source = 'window.unhandledrejection'` |
|
|
97
|
+
| Manual `AllStak.captureException` | 🛠 | `AllStak.captureException(err, ctx?)` | `POST /ingest/v1/errors` |
|
|
98
|
+
| Manual `AllStak.captureMessage` | 🛠 | `AllStak.captureMessage(msg, level)` | `/ingest/v1/logs` (`info`/`warn`); both `errors` + `logs` for `error`/`fatal` |
|
|
99
|
+
| `console.warn` / `console.error` | ✅ | `autoBreadcrumbsConsole` (default `true`); per-method via `captureConsole={ warn: true, error: true }` | `breadcrumb` of `type: 'log'`, `level: 'warn'` / `'error'`, `data.category: 'console'` |
|
|
100
|
+
| `console.log` / `console.info` | ⚙️ | `captureConsole={{ log: true, info: true }}` (default `false` to avoid spam) | `breadcrumb` of `type: 'log'`, `level: 'info'`, `data.method: 'log'` / `'info'` |
|
|
101
|
+
| HTTP `fetch` breadcrumbs (success) | ✅ | `autoBreadcrumbsFetch` (default `true`) | `breadcrumb` of `type: 'http'`, `level: 'info'` |
|
|
102
|
+
| HTTP `fetch` 4xx / 5xx breadcrumbs | ✅ | `autoBreadcrumbsFetch` | `breadcrumb` of `type: 'http'`, `level: 'error'` when status ≥ 400 |
|
|
103
|
+
| HTTP `fetch` network failure | ✅ | `autoBreadcrumbsFetch` | `breadcrumb` with `data.error`; original error rethrown |
|
|
104
|
+
| HTTP `XMLHttpRequest` breadcrumbs | ✅ | `enableHttpTracking` (also covers fetch full events) | breadcrumb same shape |
|
|
105
|
+
| Full HTTP request events (method / URL / headers / body) | ⚙️ | `enableHttpTracking: true` (default `false`); `httpTracking: { ... }` for redaction control | `POST /ingest/v1/http-requests` |
|
|
106
|
+
| Recent failed HTTP attached to next exception | ⚙️ | `enableHttpTracking: true` | `metadata['http.recentFailed']` (last 5 failed requests) |
|
|
107
|
+
| Auto navigation breadcrumbs (pushState / popstate) | ✅ | `autoBreadcrumbsNavigation` (default `true`) | `breadcrumb` of `type: 'navigation'`, `data.from`/`data.to` |
|
|
108
|
+
| React Router integration | 🛠 | `instrumentReactRouter(useLocation())` in a `useEffect` | same shape |
|
|
109
|
+
| Next.js Pages Router | 🛠 | `instrumentNextRouter(url)` from `Router.events.on('routeChangeComplete')` | same shape |
|
|
110
|
+
| **Web Vitals (CLS / LCP / INP / FCP / TTFB)** | ✅ | `autoWebVitals` (default `true`) | `POST /ingest/v1/logs` with `metadata.category = 'web-vital'`, `metadata.name`, `metadata.value` |
|
|
111
|
+
| User context | 🛠 | `AllStak.setUser({ id, email })` | `payload.user` |
|
|
112
|
+
| Custom tags / extras / contexts | 🛠 | `AllStak.setTag` / `setExtra` / `setContext` | `payload.metadata.<key>` |
|
|
113
|
+
| `release`, `environment`, `dist` | 🛠 via init | `<AllStakProvider release="web@1.0.0">` etc. | top-level fields on every payload |
|
|
114
|
+
| Source maps | 🛠 build-time | `@allstak/js/sourcemaps` plugin (Vite / Webpack / Next) | injected `debugId` per chunk |
|
|
115
|
+
| Session replay (DOM-mutation) | ⚙️ | `replay={ sampleRate: 0.1, maskAllInputs: true }` (default OFF) | `POST /ingest/v1/replay` |
|
|
116
|
+
| Offline event queue (in-memory only — see roadmap) | ✅ | n/a; configured via transport (max 100 in-RAM, drops oldest) | retried on next successful send; lost on page navigation |
|
|
117
|
+
|
|
118
|
+
### Roadmap
|
|
119
|
+
|
|
120
|
+
- Persistent offline queue (currently RAM-only; lost on page navigation)
|
|
121
|
+
- React Query / SWR breadcrumbs
|
|
122
|
+
- Vue / Svelte / Solid sister packages
|
|
123
|
+
|
|
124
|
+
## Verification status
|
|
125
|
+
|
|
126
|
+
The capabilities marked ✅ in the matrix above were verified live in
|
|
127
|
+
Chrome against the SDK's own wire format (see
|
|
128
|
+
[docs/reports/react-sdk-production-readiness.md](docs/reports/react-sdk-production-readiness.md)
|
|
129
|
+
for command-by-command details). 156 error events, 84 logs (including
|
|
130
|
+
5 web vitals), 9 full HTTP events, and a working render-error → fallback
|
|
131
|
+
→ resetError → recovery cycle were all observed end-to-end.
|
|
132
|
+
|
|
133
|
+
The live-backend round-trip is documented in the report. The wire format
|
|
134
|
+
is identical to `@allstak/react-native`, which was fully round-trip
|
|
135
|
+
verified against the same backend in
|
|
136
|
+
`../../allstak-react-native/docs/reports/react-native-backend-ingestion-verification.md`.
|
|
137
|
+
|
|
138
|
+
## Manual setup (advanced)
|
|
139
|
+
|
|
140
|
+
If you need full control over initialization order, you can skip
|
|
141
|
+
`AllStakProvider` and set up manually:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
47
144
|
import { AllStak, AllStakErrorBoundary } from '@allstak/react';
|
|
48
145
|
|
|
49
146
|
AllStak.init({
|
|
50
|
-
apiKey:
|
|
147
|
+
apiKey: 'ask_live_…',
|
|
51
148
|
environment: 'production',
|
|
52
149
|
});
|
|
53
150
|
|
|
54
|
-
AllStak.captureException(new Error('test
|
|
151
|
+
AllStak.captureException(new Error('test'));
|
|
55
152
|
|
|
56
153
|
export function App() {
|
|
57
154
|
return (
|
|
@@ -62,8 +159,6 @@ export function App() {
|
|
|
62
159
|
}
|
|
63
160
|
```
|
|
64
161
|
|
|
65
|
-
Load the app — the test error appears in your dashboard within seconds.
|
|
66
|
-
|
|
67
162
|
## Get Your API Key
|
|
68
163
|
|
|
69
164
|
1. Sign up at [app.allstak.sa](https://app.allstak.sa)
|
|
@@ -104,7 +199,7 @@ Wrap a component with the profiler:
|
|
|
104
199
|
|
|
105
200
|
```tsx
|
|
106
201
|
import { withAllStakProfiler } from '@allstak/react';
|
|
107
|
-
export default withAllStakProfiler(Dashboard,
|
|
202
|
+
export default withAllStakProfiler(Dashboard, 'Dashboard');
|
|
108
203
|
```
|
|
109
204
|
|
|
110
205
|
Set user context on login:
|
|
@@ -165,55 +260,126 @@ Production endpoint: `https://api.allstak.sa`. Override via `host` for self-host
|
|
|
165
260
|
AllStak.init({ apiKey: '...', host: 'https://allstak.mycorp.com' });
|
|
166
261
|
```
|
|
167
262
|
|
|
168
|
-
## Source maps
|
|
263
|
+
## Source maps — automatic via build plugin
|
|
169
264
|
|
|
170
|
-
Production stack traces are minified
|
|
171
|
-
line numbers in the AllStak dashboard you need to upload
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
265
|
+
Production stack traces are minified. To see real function names and
|
|
266
|
+
line numbers in the AllStak dashboard you need to upload your source
|
|
267
|
+
maps. The build-time tooling ships **inside `@allstak/react` itself** —
|
|
268
|
+
no extra package to install — and runs **automatically** as part of
|
|
269
|
+
your normal build via the Vite / Webpack / Next.js plugin.
|
|
175
270
|
|
|
176
|
-
|
|
271
|
+
The plugins inject a stable `debugId` into every chunk and (when a
|
|
272
|
+
token is present) upload the matching `.map` to the AllStak ingest. The
|
|
273
|
+
runtime resolver in `@allstak/react` reads `globalThis._allstakDebugIds`
|
|
274
|
+
at capture time and attaches the right debug-id to each frame, so the
|
|
275
|
+
symbolicator picks the correct map even after long-tail caching of
|
|
276
|
+
bundles.
|
|
177
277
|
|
|
178
|
-
|
|
179
|
-
npm install -D @allstak/js
|
|
180
|
-
```
|
|
278
|
+
### Vite
|
|
181
279
|
|
|
182
280
|
```ts
|
|
183
281
|
// vite.config.ts
|
|
184
282
|
import { defineConfig } from 'vite';
|
|
185
|
-
import
|
|
283
|
+
import react from '@vitejs/plugin-react';
|
|
284
|
+
import { allstakSourcemaps } from '@allstak/react/vite';
|
|
186
285
|
|
|
187
286
|
export default defineConfig({
|
|
188
|
-
build: { sourcemap:
|
|
287
|
+
build: { sourcemap: true },
|
|
189
288
|
plugins: [
|
|
289
|
+
react(),
|
|
190
290
|
allstakSourcemaps({
|
|
191
|
-
release: process.env.
|
|
291
|
+
release: process.env.ALLSTAK_RELEASE!,
|
|
192
292
|
token: process.env.ALLSTAK_UPLOAD_TOKEN!,
|
|
293
|
+
dist: 'web',
|
|
193
294
|
}),
|
|
194
295
|
],
|
|
195
296
|
});
|
|
196
297
|
```
|
|
197
298
|
|
|
198
|
-
### Webpack
|
|
299
|
+
### Webpack
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
// webpack.config.js
|
|
303
|
+
const { AllStakWebpackPlugin } = require('@allstak/react/webpack');
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
devtool: 'source-map',
|
|
307
|
+
plugins: [
|
|
308
|
+
new AllStakWebpackPlugin({
|
|
309
|
+
release: process.env.ALLSTAK_RELEASE,
|
|
310
|
+
token: process.env.ALLSTAK_UPLOAD_TOKEN,
|
|
311
|
+
dist: 'web',
|
|
312
|
+
}),
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Next.js
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
// next.config.js
|
|
321
|
+
const { withAllStak } = require('@allstak/react/next');
|
|
322
|
+
|
|
323
|
+
module.exports = withAllStak(
|
|
324
|
+
{
|
|
325
|
+
release: process.env.ALLSTAK_RELEASE,
|
|
326
|
+
token: process.env.ALLSTAK_UPLOAD_TOKEN,
|
|
327
|
+
dist: 'web',
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
reactStrictMode: true,
|
|
331
|
+
// …rest of your Next config…
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The `withAllStak` wrapper sets `productionBrowserSourceMaps: true` for
|
|
337
|
+
you and only attaches the plugin to the client bundle (Node-side stack
|
|
338
|
+
traces are already symbolicated).
|
|
199
339
|
|
|
200
|
-
|
|
201
|
-
any other bundler call the underlying API after your build:
|
|
340
|
+
### Other bundlers / programmatic use
|
|
202
341
|
|
|
203
342
|
```ts
|
|
204
|
-
import { processBuildOutput } from '@allstak/
|
|
343
|
+
import { processBuildOutput } from '@allstak/react/sourcemaps';
|
|
205
344
|
|
|
206
345
|
await processBuildOutput({
|
|
207
346
|
dir: 'dist',
|
|
208
|
-
release: process.env.
|
|
347
|
+
release: process.env.ALLSTAK_RELEASE!,
|
|
209
348
|
token: process.env.ALLSTAK_UPLOAD_TOKEN!,
|
|
349
|
+
dist: 'web',
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### What gets sent
|
|
354
|
+
|
|
355
|
+
Each map is POSTed as `multipart/form-data` to
|
|
356
|
+
`POST /api/v1/artifacts/upload` with these fields:
|
|
357
|
+
|
|
358
|
+
- `debugId` — UUID injected into the bundle and the map (matching across both)
|
|
359
|
+
- `type` — `"sourcemap"` (and `"bundle"` if `uploadBundles: true`)
|
|
360
|
+
- `release` — your release identifier (e.g. `web@1.4.2`)
|
|
361
|
+
- `dist` — your distribution tag (e.g. `web`)
|
|
362
|
+
- `file` — the map content as a Blob
|
|
363
|
+
|
|
364
|
+
Auth uses `X-AllStak-Upload-Token` (a project-scoped upload token, NOT
|
|
365
|
+
the runtime API key). Generate one in **Project Settings → Upload Tokens**.
|
|
366
|
+
|
|
367
|
+
### Privacy
|
|
368
|
+
|
|
369
|
+
By default the map is uploaded as-is, which includes embedded
|
|
370
|
+
`sourcesContent`. To strip it (smaller payloads, no source code stored
|
|
371
|
+
on AllStak's side), set `stripSources: true`:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
allstakSourcemaps({
|
|
375
|
+
release: '…',
|
|
376
|
+
token: '…',
|
|
377
|
+
stripSources: true,
|
|
210
378
|
});
|
|
211
379
|
```
|
|
212
380
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
to attach the right debug-id to each captured frame, so the symbolicator
|
|
216
|
-
picks the correct map even after long-tail caching of bundles.
|
|
381
|
+
The backend rejects uploads in privacy-mode projects when the map still
|
|
382
|
+
contains `sourcesContent` — the SDK side surfaces a clear error.
|
|
217
383
|
|
|
218
384
|
## Links
|
|
219
385
|
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// src/build/walk.ts
|
|
2
|
+
import { readdirSync, statSync } from "fs";
|
|
3
|
+
import { basename, join } from "path";
|
|
4
|
+
function walk(dir, out = []) {
|
|
5
|
+
for (const name of readdirSync(dir)) {
|
|
6
|
+
const full = join(dir, name);
|
|
7
|
+
const st = statSync(full);
|
|
8
|
+
if (st.isDirectory()) walk(full, out);
|
|
9
|
+
else out.push(full);
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
function findPairs(root) {
|
|
14
|
+
const all = walk(root);
|
|
15
|
+
const maps = new Set(all.filter((p) => p.endsWith(".map")));
|
|
16
|
+
const pairs = [];
|
|
17
|
+
for (const js of all) {
|
|
18
|
+
if (!js.endsWith(".js") && !js.endsWith(".mjs") && !js.endsWith(".cjs")) continue;
|
|
19
|
+
const map = js + ".map";
|
|
20
|
+
if (maps.has(map)) {
|
|
21
|
+
pairs.push({ jsPath: js, mapPath: map, bundleName: basename(js) });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return pairs;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/build/inject.ts
|
|
28
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
29
|
+
import { randomUUID } from "crypto";
|
|
30
|
+
var DEBUG_ID_LINE_RE = /^\/\/# debugId=([0-9a-f-]{36})\s*$/m;
|
|
31
|
+
var REGISTRATION_MARKER = "/*!__allstak_debug_id_registration__*/";
|
|
32
|
+
function buildRegistrationSnippet(jsBody, debugId) {
|
|
33
|
+
const isEsm = /\bimport\.meta\b/.test(jsBody) || /^\s*(?:import|export)\b/m.test(jsBody);
|
|
34
|
+
if (isEsm) {
|
|
35
|
+
return `${REGISTRATION_MARKER}try{(globalThis._allstakDebugIds=globalThis._allstakDebugIds||{})[import.meta.url]="${debugId}"}catch(_){}`;
|
|
36
|
+
}
|
|
37
|
+
return `${REGISTRATION_MARKER}(function(){try{var u=(typeof document!=="undefined"&&document.currentScript&&document.currentScript.src)||(typeof location!=="undefined"?location.href:"");(globalThis._allstakDebugIds=globalThis._allstakDebugIds||{})[u]="${debugId}"}catch(_){}})();`;
|
|
38
|
+
}
|
|
39
|
+
function stripRegistration(js) {
|
|
40
|
+
const lineRe = new RegExp(
|
|
41
|
+
"^" + REGISTRATION_MARKER.replace(/[/*!]/g, (c) => "\\" + c) + ".*$",
|
|
42
|
+
"m"
|
|
43
|
+
);
|
|
44
|
+
return js.replace(lineRe, "");
|
|
45
|
+
}
|
|
46
|
+
function injectPair(p) {
|
|
47
|
+
const jsRaw = readFileSync(p.jsPath, "utf8");
|
|
48
|
+
const mapRaw = readFileSync(p.mapPath, "utf8");
|
|
49
|
+
const map = JSON.parse(mapRaw);
|
|
50
|
+
let debugId = typeof map.debugId === "string" ? map.debugId : "";
|
|
51
|
+
const existing = DEBUG_ID_LINE_RE.exec(jsRaw);
|
|
52
|
+
if (existing && existing[1]) debugId = debugId || existing[1];
|
|
53
|
+
const reused = !!debugId;
|
|
54
|
+
if (!debugId) debugId = randomUUID();
|
|
55
|
+
map.debugId = debugId;
|
|
56
|
+
writeFileSync(p.mapPath, JSON.stringify(map));
|
|
57
|
+
let jsOut = stripRegistration(jsRaw.replace(DEBUG_ID_LINE_RE, ""));
|
|
58
|
+
jsOut = jsOut.replace(/\s+$/, "");
|
|
59
|
+
jsOut += `
|
|
60
|
+
${buildRegistrationSnippet(jsOut, debugId)}
|
|
61
|
+
//# debugId=${debugId}
|
|
62
|
+
`;
|
|
63
|
+
writeFileSync(p.jsPath, jsOut);
|
|
64
|
+
return { debugId, reused };
|
|
65
|
+
}
|
|
66
|
+
function injectAll(pairs) {
|
|
67
|
+
return pairs.map((pair) => ({ pair, result: injectPair(pair) }));
|
|
68
|
+
}
|
|
69
|
+
function readDebugIdFromMap(mapPath) {
|
|
70
|
+
const json = JSON.parse(readFileSync(mapPath, "utf8"));
|
|
71
|
+
return typeof json.debugId === "string" ? json.debugId : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/build/upload.ts
|
|
75
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
76
|
+
import { basename as basename2 } from "path";
|
|
77
|
+
var DEFAULT_HOST = "https://api.allstak.sa";
|
|
78
|
+
async function uploadOne(type, filePath, debugId, opts) {
|
|
79
|
+
let buf = readFileSync2(filePath);
|
|
80
|
+
if (type === "sourcemap" && opts.stripSources) {
|
|
81
|
+
const json = JSON.parse(buf.toString("utf8"));
|
|
82
|
+
if (Array.isArray(json.sourcesContent)) delete json.sourcesContent;
|
|
83
|
+
buf = Buffer.from(JSON.stringify(json));
|
|
84
|
+
}
|
|
85
|
+
const form = new FormData();
|
|
86
|
+
form.append("debugId", debugId);
|
|
87
|
+
form.append("type", type);
|
|
88
|
+
form.append("release", opts.release);
|
|
89
|
+
if (opts.dist) form.append("dist", opts.dist);
|
|
90
|
+
form.append(
|
|
91
|
+
"file",
|
|
92
|
+
new Blob([buf], {
|
|
93
|
+
type: type === "sourcemap" ? "application/json" : "application/javascript"
|
|
94
|
+
}),
|
|
95
|
+
basename2(filePath)
|
|
96
|
+
);
|
|
97
|
+
const res = await fetch(opts.host.replace(/\/$/, "") + "/api/v1/artifacts/upload", {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "X-AllStak-Upload-Token": opts.token },
|
|
100
|
+
body: form
|
|
101
|
+
});
|
|
102
|
+
return { status: res.status, body: await res.text(), ok: res.ok };
|
|
103
|
+
}
|
|
104
|
+
function sha8(buf) {
|
|
105
|
+
let hash = 0;
|
|
106
|
+
for (let i = 0; i < buf.length; i++) hash = (hash << 5) - hash + buf[i] | 0;
|
|
107
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
108
|
+
}
|
|
109
|
+
async function uploadPair(p, opts) {
|
|
110
|
+
const debugId = readDebugIdFromMap(p.mapPath);
|
|
111
|
+
if (!debugId) {
|
|
112
|
+
return {
|
|
113
|
+
bundleName: p.bundleName,
|
|
114
|
+
debugId: "",
|
|
115
|
+
ok: false,
|
|
116
|
+
steps: [{
|
|
117
|
+
type: "sourcemap",
|
|
118
|
+
status: 0,
|
|
119
|
+
sha8: "",
|
|
120
|
+
body: `[allstak/upload] no debugId in ${p.mapPath} \u2014 run inject first`
|
|
121
|
+
}]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const merged = {
|
|
125
|
+
release: opts.release,
|
|
126
|
+
host: opts.host ?? process.env.ALLSTAK_HOST ?? DEFAULT_HOST,
|
|
127
|
+
token: opts.token,
|
|
128
|
+
dist: opts.dist,
|
|
129
|
+
stripSources: opts.stripSources
|
|
130
|
+
};
|
|
131
|
+
const steps = [];
|
|
132
|
+
const mapResult = await uploadOne("sourcemap", p.mapPath, debugId, merged);
|
|
133
|
+
steps.push({
|
|
134
|
+
type: "sourcemap",
|
|
135
|
+
status: mapResult.status,
|
|
136
|
+
sha8: sha8(readFileSync2(p.mapPath)),
|
|
137
|
+
body: mapResult.ok ? void 0 : mapResult.body
|
|
138
|
+
});
|
|
139
|
+
let allOk = mapResult.ok;
|
|
140
|
+
if (opts.uploadBundles) {
|
|
141
|
+
const bundleResult = await uploadOne("bundle", p.jsPath, debugId, merged);
|
|
142
|
+
steps.push({
|
|
143
|
+
type: "bundle",
|
|
144
|
+
status: bundleResult.status,
|
|
145
|
+
sha8: sha8(readFileSync2(p.jsPath)),
|
|
146
|
+
body: bundleResult.ok ? void 0 : bundleResult.body
|
|
147
|
+
});
|
|
148
|
+
allOk = allOk && bundleResult.ok;
|
|
149
|
+
}
|
|
150
|
+
return { bundleName: p.bundleName, debugId, ok: allOk, steps };
|
|
151
|
+
}
|
|
152
|
+
async function uploadAll(pairs, opts) {
|
|
153
|
+
return Promise.all(pairs.map((p) => uploadPair(p, opts)));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/build/sourcemaps.ts
|
|
157
|
+
import { resolve } from "path";
|
|
158
|
+
async function processBuildOutput(opts) {
|
|
159
|
+
const dir = resolve(opts.dir);
|
|
160
|
+
const pairs = findPairs(dir);
|
|
161
|
+
const log = opts.silent ? () => void 0 : (m) => console.log(`[allstak/sourcemaps] ${m}`);
|
|
162
|
+
log(`scanning ${dir} \u2014 ${pairs.length} bundle/map pair(s)`);
|
|
163
|
+
if (pairs.length === 0) {
|
|
164
|
+
return { dir, pairs: 0, injected: [] };
|
|
165
|
+
}
|
|
166
|
+
const injectedRaw = injectAll(pairs);
|
|
167
|
+
const injected = injectedRaw.map(({ pair, result }) => ({
|
|
168
|
+
bundleName: pair.bundleName,
|
|
169
|
+
debugId: result.debugId,
|
|
170
|
+
reused: result.reused
|
|
171
|
+
}));
|
|
172
|
+
for (const i of injected) {
|
|
173
|
+
log(` ${i.bundleName} ${i.debugId} ${i.reused ? "(reused)" : "(new)"}`);
|
|
174
|
+
}
|
|
175
|
+
const token = opts.token ?? process.env.ALLSTAK_UPLOAD_TOKEN;
|
|
176
|
+
const release = opts.release ?? process.env.ALLSTAK_RELEASE;
|
|
177
|
+
if (opts.injectOnly || !token) {
|
|
178
|
+
if (!opts.injectOnly && !token) {
|
|
179
|
+
log("skipping upload \u2014 no token (set ALLSTAK_UPLOAD_TOKEN or pass `token`)");
|
|
180
|
+
}
|
|
181
|
+
return { dir, pairs: pairs.length, injected };
|
|
182
|
+
}
|
|
183
|
+
if (!release) {
|
|
184
|
+
log("skipping upload \u2014 no release (set ALLSTAK_RELEASE or pass `release`)");
|
|
185
|
+
return { dir, pairs: pairs.length, injected };
|
|
186
|
+
}
|
|
187
|
+
const uploaded = await uploadAll(pairs, {
|
|
188
|
+
...opts,
|
|
189
|
+
release,
|
|
190
|
+
token
|
|
191
|
+
});
|
|
192
|
+
for (const u of uploaded) {
|
|
193
|
+
if (u.ok) {
|
|
194
|
+
log(` ${u.bundleName} uploaded debugId=${u.debugId}`);
|
|
195
|
+
} else {
|
|
196
|
+
const last = u.steps[u.steps.length - 1];
|
|
197
|
+
log(` ${u.bundleName} FAIL status=${last?.status ?? "?"} body=${last?.body ?? ""}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { dir, pairs: pairs.length, injected, uploaded };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
walk,
|
|
205
|
+
findPairs,
|
|
206
|
+
injectPair,
|
|
207
|
+
injectAll,
|
|
208
|
+
readDebugIdFromMap,
|
|
209
|
+
DEFAULT_HOST,
|
|
210
|
+
uploadPair,
|
|
211
|
+
uploadAll,
|
|
212
|
+
processBuildOutput
|
|
213
|
+
};
|
|
214
|
+
//# sourceMappingURL=chunk-G6VPGDP6.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/build/walk.ts","../../src/build/inject.ts","../../src/build/upload.ts","../../src/build/sourcemaps.ts"],"sourcesContent":["/**\n * Build-time only. File-system walking for the source-map pipeline.\n * Pure Node 18+ (built-in `node:fs` only). Browser runtime never imports\n * this — it's behind a `./build/*` subpath that's marked Node-platform.\n */\n\nimport { readdirSync, statSync } from 'node:fs';\nimport { basename, join } from 'node:path';\n\n/** A bundle and its companion source map on disk. */\nexport interface BundlePair {\n /** Absolute path to the JS bundle (`.js` / `.mjs` / `.cjs`). */\n jsPath: string;\n /** Absolute path to the matching `.map` file. */\n mapPath: string;\n /** Bare filename of the bundle (no directory), for log lines. */\n bundleName: string;\n}\n\n/** Recursively list every file under `dir`. Symlinks are followed. */\nexport function walk(dir: string, out: string[] = []): string[] {\n for (const name of readdirSync(dir)) {\n const full = join(dir, name);\n const st = statSync(full);\n if (st.isDirectory()) walk(full, out);\n else out.push(full);\n }\n return out;\n}\n\n/**\n * Returns every `(bundle, sourcemap)` pair under `root`.\n *\n * A pair is a `.js` / `.mjs` / `.cjs` file with a sibling file of the\n * same name plus a `.map` suffix — the convention every modern bundler\n * (Vite, Webpack, esbuild, Rollup, tsup) follows.\n */\nexport function findPairs(root: string): BundlePair[] {\n const all = walk(root);\n const maps = new Set(all.filter((p) => p.endsWith('.map')));\n const pairs: BundlePair[] = [];\n for (const js of all) {\n if (!js.endsWith('.js') && !js.endsWith('.mjs') && !js.endsWith('.cjs')) continue;\n const map = js + '.map';\n if (maps.has(map)) {\n pairs.push({ jsPath: js, mapPath: map, bundleName: basename(js) });\n }\n }\n return pairs;\n}\n","/**\n * Debug-ID injection. Build-time only.\n *\n * For each `(bundle.js, bundle.js.map)` pair we:\n * - Generate a stable per-bundle UUID (one already on the bundle is\n * reused so re-running is idempotent).\n * - Append `//# debugId=<uuid>` to the JS so the runtime resolver in\n * `src/debug-id.ts` can read it back.\n * - Write a top-level `debugId` field into the `.map` JSON so the\n * symbolicator on the backend can join `bundle.js` ↔ `bundle.js.map`\n * by ID rather than guessing from filenames.\n *\n * Bundlers re-write hashed filenames on every build, so joining by ID\n * (instead of by URL or path) is what makes resolved stack frames\n * survive across releases.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\n\nimport type { BundlePair } from './walk';\n\nconst DEBUG_ID_LINE_RE = /^\\/\\/# debugId=([0-9a-f-]{36})\\s*$/m;\nconst REGISTRATION_MARKER = '/*!__allstak_debug_id_registration__*/';\n\nfunction buildRegistrationSnippet(jsBody: string, debugId: string): string {\n const isEsm = /\\bimport\\.meta\\b/.test(jsBody) || /^\\s*(?:import|export)\\b/m.test(jsBody);\n if (isEsm) {\n return `${REGISTRATION_MARKER}try{(globalThis._allstakDebugIds=globalThis._allstakDebugIds||{})[import.meta.url]=\"${debugId}\"}catch(_){}`;\n }\n return `${REGISTRATION_MARKER}(function(){try{var u=(typeof document!==\"undefined\"&&document.currentScript&&document.currentScript.src)||(typeof location!==\"undefined\"?location.href:\"\");(globalThis._allstakDebugIds=globalThis._allstakDebugIds||{})[u]=\"${debugId}\"}catch(_){}})();`;\n}\n\nfunction stripRegistration(js: string): string {\n const lineRe = new RegExp(\n '^' + REGISTRATION_MARKER.replace(/[/*!]/g, (c) => '\\\\' + c) + '.*$',\n 'm',\n );\n return js.replace(lineRe, '');\n}\n\n/** Outcome of injecting one pair. */\nexport interface InjectResult {\n /** UUID injected (or reused) for this bundle. */\n debugId: string;\n /** True if the bundle already had a debugId — we reused it. */\n reused: boolean;\n}\n\n/**\n * Inject (or reuse) the debug ID for a single bundle/sourcemap pair.\n * Mutates both files on disk. Pure synchronous Node — safe to call from\n * a Vite `closeBundle` or Webpack `afterEmit` hook.\n */\nexport function injectPair(p: BundlePair): InjectResult {\n const jsRaw = readFileSync(p.jsPath, 'utf8');\n const mapRaw = readFileSync(p.mapPath, 'utf8');\n const map = JSON.parse(mapRaw) as { debugId?: unknown; [k: string]: unknown };\n\n let debugId = typeof map.debugId === 'string' ? map.debugId : '';\n const existing = DEBUG_ID_LINE_RE.exec(jsRaw);\n if (existing && existing[1]) debugId = debugId || existing[1];\n const reused = !!debugId;\n if (!debugId) debugId = randomUUID();\n\n map.debugId = debugId;\n writeFileSync(p.mapPath, JSON.stringify(map));\n\n let jsOut = stripRegistration(jsRaw.replace(DEBUG_ID_LINE_RE, ''));\n jsOut = jsOut.replace(/\\s+$/, '');\n jsOut += `\\n${buildRegistrationSnippet(jsOut, debugId)}\\n//# debugId=${debugId}\\n`;\n writeFileSync(p.jsPath, jsOut);\n\n return { debugId, reused };\n}\n\n/** Inject every pair under `root`. Returns one record per pair. */\nexport function injectAll(pairs: BundlePair[]): Array<{ pair: BundlePair; result: InjectResult }> {\n return pairs.map((pair) => ({ pair, result: injectPair(pair) }));\n}\n\n/** Read a debug ID back from a `.map` file. */\nexport function readDebugIdFromMap(mapPath: string): string | null {\n const json = JSON.parse(readFileSync(mapPath, 'utf8')) as { debugId?: unknown };\n return typeof json.debugId === 'string' ? json.debugId : null;\n}\n","/**\n * Source-map / bundle upload client. Build-time only.\n *\n * Wraps the AllStak `/api/v1/artifacts/upload` endpoint with multipart\n * form data and best-effort retries. Pure Node 18+ (uses the global\n * `fetch` and `FormData`), no third-party HTTP client required.\n *\n * Auth: `X-AllStak-Upload-Token` header (NOT the runtime API key —\n * uploads have a dedicated, narrower scope. Generate one in the\n * dashboard → Project Settings → Upload Tokens).\n */\n\nimport { readFileSync } from 'node:fs';\nimport { basename } from 'node:path';\n\nimport type { BundlePair } from './walk';\nimport { readDebugIdFromMap } from './inject';\n\n/** Default ingest host — overridden via `host` option or `ALLSTAK_HOST`. */\nexport const DEFAULT_HOST = 'https://api.allstak.sa';\n\n/** Options for {@link uploadAll} / {@link uploadPair}. */\nexport interface UploadOptions {\n /** Release identifier, e.g. `myapp@1.4.2`. Required server-side. */\n release: string;\n /** Optional distribution tag (`web`, `ios-hermes`, `staging`, …). */\n dist?: string;\n /** AllStak ingest host (default `https://api.allstak.sa`). */\n host?: string;\n /** Project upload token (`aspk_…`). May come from `ALLSTAK_UPLOAD_TOKEN`. */\n token: string;\n /**\n * Drop `sourcesContent` from the map before upload (smaller payload).\n * Off by default — sourcesContent enables full-source rendering on the\n * dashboard. Turn on if you don't want source code uploaded.\n */\n stripSources?: boolean;\n /** Also upload the JS bundle alongside the map (off by default). */\n uploadBundles?: boolean;\n}\n\n/** One artifact upload result. */\nexport interface UploadResult {\n bundleName: string;\n debugId: string;\n /** True when both the map (and bundle, if requested) uploaded OK. */\n ok: boolean;\n /** Per-artifact responses, in the order we sent them. */\n steps: Array<{\n type: 'sourcemap' | 'bundle';\n status: number;\n sha8: string;\n body?: string;\n }>;\n}\n\ninterface OneStepResult {\n status: number;\n body: string;\n ok: boolean;\n}\n\nasync function uploadOne(\n type: 'sourcemap' | 'bundle',\n filePath: string,\n debugId: string,\n opts: Required<Pick<UploadOptions, 'release' | 'host' | 'token'>> &\n Pick<UploadOptions, 'dist' | 'stripSources'>,\n): Promise<OneStepResult> {\n let buf = readFileSync(filePath);\n if (type === 'sourcemap' && opts.stripSources) {\n const json = JSON.parse(buf.toString('utf8')) as { sourcesContent?: unknown };\n if (Array.isArray(json.sourcesContent)) delete json.sourcesContent;\n buf = Buffer.from(JSON.stringify(json));\n }\n\n const form = new FormData();\n form.append('debugId', debugId);\n form.append('type', type);\n form.append('release', opts.release);\n if (opts.dist) form.append('dist', opts.dist);\n form.append(\n 'file',\n new Blob([buf], {\n type: type === 'sourcemap' ? 'application/json' : 'application/javascript',\n }),\n basename(filePath),\n );\n\n const res = await fetch(opts.host.replace(/\\/$/, '') + '/api/v1/artifacts/upload', {\n method: 'POST',\n headers: { 'X-AllStak-Upload-Token': opts.token },\n body: form,\n });\n return { status: res.status, body: await res.text(), ok: res.ok };\n}\n\nfunction sha8(buf: Buffer): string {\n // Browser/Node fallback — keeps this file pure-JS (no node:crypto in\n // the public surface). Caller doesn't need cryptographic strength;\n // this is for log identification only.\n let hash = 0;\n for (let i = 0; i < buf.length; i++) hash = ((hash << 5) - hash + buf[i]!) | 0;\n return (hash >>> 0).toString(16).padStart(8, '0');\n}\n\n/** Upload one bundle/map pair. The map is always uploaded; bundle is opt-in. */\nexport async function uploadPair(p: BundlePair, opts: UploadOptions): Promise<UploadResult> {\n const debugId = readDebugIdFromMap(p.mapPath);\n if (!debugId) {\n return {\n bundleName: p.bundleName,\n debugId: '',\n ok: false,\n steps: [{\n type: 'sourcemap',\n status: 0,\n sha8: '',\n body: `[allstak/upload] no debugId in ${p.mapPath} — run inject first`,\n }],\n };\n }\n const merged = {\n release: opts.release,\n host: opts.host ?? process.env.ALLSTAK_HOST ?? DEFAULT_HOST,\n token: opts.token,\n dist: opts.dist,\n stripSources: opts.stripSources,\n };\n\n const steps: UploadResult['steps'] = [];\n const mapResult = await uploadOne('sourcemap', p.mapPath, debugId, merged);\n steps.push({\n type: 'sourcemap',\n status: mapResult.status,\n sha8: sha8(readFileSync(p.mapPath)),\n body: mapResult.ok ? undefined : mapResult.body,\n });\n\n let allOk = mapResult.ok;\n if (opts.uploadBundles) {\n const bundleResult = await uploadOne('bundle', p.jsPath, debugId, merged);\n steps.push({\n type: 'bundle',\n status: bundleResult.status,\n sha8: sha8(readFileSync(p.jsPath)),\n body: bundleResult.ok ? undefined : bundleResult.body,\n });\n allOk = allOk && bundleResult.ok;\n }\n\n return { bundleName: p.bundleName, debugId, ok: allOk, steps };\n}\n\n/** Upload every pair in parallel. */\nexport async function uploadAll(\n pairs: BundlePair[],\n opts: UploadOptions,\n): Promise<UploadResult[]> {\n return Promise.all(pairs.map((p) => uploadPair(p, opts)));\n}\n","/**\n * Programmatic source-map pipeline. Build-time only.\n *\n * The high-level \"do everything\" entry point Vite/Webpack/Next plugins\n * call. Walk the build output → inject debug IDs → upload artifacts.\n *\n * import { processBuildOutput } from '@allstak/react/sourcemaps';\n *\n * await processBuildOutput({\n * dir: 'dist',\n * release: 'web@1.4.2',\n * token: process.env.ALLSTAK_UPLOAD_TOKEN!,\n * });\n */\n\nexport type { BundlePair } from './walk';\nexport { findPairs, walk } from './walk';\nexport type { InjectResult } from './inject';\nexport { injectPair, injectAll, readDebugIdFromMap } from './inject';\nexport type { UploadOptions, UploadResult } from './upload';\nexport { uploadPair, uploadAll, DEFAULT_HOST } from './upload';\n\nimport { resolve } from 'node:path';\nimport { findPairs } from './walk';\nimport { injectAll } from './inject';\nimport { uploadAll, type UploadOptions, type UploadResult } from './upload';\n\n/** Options for {@link processBuildOutput}. */\nexport interface ProcessOptions extends Partial<UploadOptions> {\n /** Build output directory to scan (e.g. `dist`, `.next/static`). */\n dir: string;\n /**\n * If true, only inject debug IDs and skip upload. Useful for sample\n * apps and CI dry-runs.\n */\n injectOnly?: boolean;\n /** Suppress per-pair console output. Default false (you want to see this). */\n silent?: boolean;\n}\n\n/** What `processBuildOutput` reports back. */\nexport interface ProcessReport {\n /** Absolute output dir scanned. */\n dir: string;\n /** Pairs found. */\n pairs: number;\n /** Per-pair injection results (debugId + reused?). */\n injected: Array<{ bundleName: string; debugId: string; reused: boolean }>;\n /** Per-pair upload results, omitted when no token / `injectOnly: true`. */\n uploaded?: UploadResult[];\n}\n\nexport async function processBuildOutput(opts: ProcessOptions): Promise<ProcessReport> {\n const dir = resolve(opts.dir);\n const pairs = findPairs(dir);\n const log = opts.silent ? () => undefined : (m: string) => console.log(`[allstak/sourcemaps] ${m}`);\n\n log(`scanning ${dir} — ${pairs.length} bundle/map pair(s)`);\n if (pairs.length === 0) {\n return { dir, pairs: 0, injected: [] };\n }\n\n const injectedRaw = injectAll(pairs);\n const injected = injectedRaw.map(({ pair, result }) => ({\n bundleName: pair.bundleName,\n debugId: result.debugId,\n reused: result.reused,\n }));\n for (const i of injected) {\n log(` ${i.bundleName} ${i.debugId} ${i.reused ? '(reused)' : '(new)'}`);\n }\n\n const token = opts.token ?? process.env.ALLSTAK_UPLOAD_TOKEN;\n const release = opts.release ?? process.env.ALLSTAK_RELEASE;\n\n if (opts.injectOnly || !token) {\n if (!opts.injectOnly && !token) {\n log('skipping upload — no token (set ALLSTAK_UPLOAD_TOKEN or pass `token`)');\n }\n return { dir, pairs: pairs.length, injected };\n }\n if (!release) {\n log('skipping upload — no release (set ALLSTAK_RELEASE or pass `release`)');\n return { dir, pairs: pairs.length, injected };\n }\n\n const uploaded = await uploadAll(pairs, {\n ...opts,\n release,\n token,\n });\n for (const u of uploaded) {\n if (u.ok) {\n log(` ${u.bundleName} uploaded debugId=${u.debugId}`);\n } else {\n const last = u.steps[u.steps.length - 1];\n log(` ${u.bundleName} FAIL status=${last?.status ?? '?'} body=${last?.body ?? ''}`);\n }\n }\n return { dir, pairs: pairs.length, injected, uploaded };\n}\n"],"mappings":"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","names":["readFileSync","basename","readFileSync","basename"]}
|