@realuptime/errors 0.4.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.
@@ -0,0 +1,250 @@
1
+ /**
2
+ * The Errors wire contract (docs/errors-plan.md, "The SDKs: two
3
+ * languages, one contract").
4
+ *
5
+ * DUPLICATED, not imported. Same convention as apps/agent/types.ts and for
6
+ * the same reason: this package ships into customer bundles and processes,
7
+ * so its dependency graph must stay auditable at a glance -- zero imports
8
+ * beyond the platform. The cost (a server-side rename does not break this
9
+ * build) is paid by wire-contract.test.ts, which pins every field name, and
10
+ * by the shared scrub vectors (scrub-vectors.json), which pin the scrub
11
+ * semantics across this SDK, the Python SDK, and the server's second net.
12
+ * The server-side mirror of these limits lives in
13
+ * packages/db/error-ingest.ts.
14
+ *
15
+ * ## v2 (REA-182): identity, tags, custom context, device metadata
16
+ *
17
+ * STRICTLY ADDITIVE, and that word is load-bearing rather than decorative.
18
+ * Every v1 field keeps its name, its type and its meaning; every v2 field is
19
+ * OPTIONAL and absent from a v1 payload. A v1 batch produced by an SDK that
20
+ * predates this file still ingests byte-for-byte unchanged, which
21
+ * route.test.ts pins with a frozen golden v1 body.
22
+ *
23
+ * **There is deliberately no `version` field on the wire, and that is a
24
+ * decision, not an omission.** A version discriminant would have to be
25
+ * REQUIRED to be worth reading, and a required field is by construction not
26
+ * additive: every already-deployed SDK in a customer's process omits it, so
27
+ * the server would have to treat "absent" as v1 anyway -- which is exactly
28
+ * what optional-field presence already tells it, with no field to keep in
29
+ * sync. The `sdk` string ("realuptime-errors-js/0.3.0") remains the honest
30
+ * record of what produced a payload. A future change that is NOT additive
31
+ * (a removed field, a changed meaning) gets a versioned PATH, the way the
32
+ * fingerprint algorithm gets a version rather than mutating history.
33
+ */
34
+ /** One stack frame, post-scrub. `inApp` is the SDK's best-effort guess
35
+ * (Node: not under node_modules and not an internal frame); the server
36
+ * trusts it for display and for fingerprinting. */
37
+ export interface WireFrame {
38
+ file: string;
39
+ function: string | null;
40
+ line: number | null;
41
+ inApp: boolean;
42
+ /** 1-based column when the runtime provided one (Phase 3). Source-map
43
+ * symbolication needs it; a frame without one renders raw server-side
44
+ * with a stated reason, never silently pretty. */
45
+ column?: number | null;
46
+ /** Source context, captured client-side where the SDK can read source
47
+ * (the Python SDK); pattern-scrubbed before serialization. */
48
+ contextLine?: string | null;
49
+ preContext?: string[];
50
+ postContext?: string[];
51
+ /** v2 (REA-182): this frame's local variables, stringified.
52
+ *
53
+ * OFF BY DEFAULT in both SDKs and hard-capped when on
54
+ * (MAX_LOCAL_VARS_PER_FRAME entries, MAX_LOCAL_VAR_LENGTH characters
55
+ * each), because locals are the single leakiest thing an error tracker
56
+ * can capture: the variable holding the password is, definitionally, in
57
+ * scope at the frame that failed to use it. Values pass the pattern scrub
58
+ * like every other captured string, but the pattern scrub is a net, not a
59
+ * guarantee, which is why the switch defaults to off rather than to
60
+ * "trust the regexes".
61
+ *
62
+ * Captured by the Python SDK only, the same asymmetry `contextLine`
63
+ * already has: a JS stack is a STRING, and the runtime hands the SDK no
64
+ * access to the frames behind it. The field is in the contract on both
65
+ * sides so the scrub, the storage and the UI are one implementation. */
66
+ vars?: Record<string, string> | null;
67
+ }
68
+ /** v2 (REA-182): who this happened to.
69
+ *
70
+ * `id` is the field the product is built around: an opaque account or user
71
+ * identifier is what makes "how many users hit this" answerable, and it is
72
+ * the customer's own key space, not a person's contact details. It rides
73
+ * the wire as-is (pattern-scrubbed like any other string).
74
+ *
75
+ * `email` and `username` are PERSONAL DATA and are `[scrubbed]` by default,
76
+ * client-side, before serialization. A customer who genuinely needs them
77
+ * opts each one back in BY NAME in SDK config (`allowFields: ["user.email"]`)
78
+ * -- the same per-field, in-code, never-a-global-switch shape the removed
79
+ * headers use, and for the same reason: a one-switch opt-out is how a
80
+ * default dies. There is no server-side way to turn this on, deliberately:
81
+ * a default that a dashboard toggle can disable is a default that leaks
82
+ * before anyone notices. */
83
+ export interface WireUser {
84
+ id?: string | null;
85
+ email?: string | null;
86
+ username?: string | null;
87
+ }
88
+ /** v2 (REA-182): where it ran. Runtime and platform facts the SDK reads out
89
+ * of its own process, never out of the request.
90
+ *
91
+ * Deliberately NOT here: hostname, local IP, container id, or anything else
92
+ * that identifies a machine rather than a runtime. Those are operational
93
+ * identifiers with real re-identification value and no debugging value that
94
+ * `release` + `environment` does not already carry. */
95
+ export interface WireDevice {
96
+ /** "node", "python", "browser". */
97
+ runtime?: string;
98
+ /** e.g. "22.4.0", "3.12.1". */
99
+ runtimeVersion?: string;
100
+ /** e.g. "linux", "darwin", "win32". */
101
+ platform?: string;
102
+ /** e.g. "x64", "arm64". */
103
+ arch?: string;
104
+ }
105
+ /** One breadcrumb (Phase 3): a scrubbed trail entry recorded before the
106
+ * event. Newest last on the wire. */
107
+ export interface WireBreadcrumb {
108
+ /** ISO 8601, the customer clock; display-only. */
109
+ timestamp: string | null;
110
+ category: string | null;
111
+ message: string;
112
+ data: Record<string, string> | null;
113
+ }
114
+ /** Minimal request context: method, path, status by default. Headers appear
115
+ * only for fields the integrator captured deliberately, and the scrub-by-
116
+ * default rules in scrub.ts have already run over everything here before it
117
+ * is serialized. */
118
+ export interface WireRequest {
119
+ method?: string;
120
+ path?: string;
121
+ status?: number;
122
+ headers?: Record<string, string>;
123
+ /** v2 (REA-182): the matched ROUTE TEMPLATE, e.g. "/orders/:id", as
124
+ * distinct from the concrete path "/orders/4242". This is the one extra
125
+ * thing a framework adapter genuinely knows and cannot be guessed from
126
+ * the path, it is low-cardinality (it groups), and it carries no
127
+ * user-supplied bytes at all. Adapters forward it and nothing else new:
128
+ * never headers, never cookies, never bodies. Pattern-scrubbed anyway,
129
+ * because a route template is still a string a customer wrote. */
130
+ route?: string;
131
+ }
132
+ export interface WireEvent {
133
+ /** ISO 8601, the customer process's clock. The server bounds it and never
134
+ * uses it for quota. */
135
+ occurredAt: string;
136
+ message: string;
137
+ exceptionType: string | null;
138
+ release: string | null;
139
+ environment: string | null;
140
+ frames: WireFrame[] | null;
141
+ request: WireRequest | null;
142
+ /** Custom grouping override (`fingerprint: [...]`), the escape hatch for
143
+ * the cases automatic grouping gets wrong. Null means "group for me". */
144
+ fingerprint: string[] | null;
145
+ /** The breadcrumb trail preceding this event, newest last, at most
146
+ * MAX_BREADCRUMBS_PER_EVENT (Phase 3). Null when none were recorded. */
147
+ breadcrumbs: WireBreadcrumb[] | null;
148
+ /** How many older breadcrumbs the bounded client-side ring had evicted
149
+ * when this event was built: the count of what the trail is NOT showing,
150
+ * reported so the server can render "showing the last N; M older
151
+ * dropped" instead of a quietly truncated trail. */
152
+ breadcrumbsDropped: number;
153
+ /** Who this happened to. See WireUser: id rides as-is, email and username
154
+ * are scrubbed unless opted in per field. */
155
+ user?: WireUser | null;
156
+ /** Short, low-cardinality labels the customer sets (`setTag`), the things
157
+ * an issues list can filter on: "tenant", "plan", "feature-flag". Values
158
+ * pass the pattern scrub. At most MAX_TAGS_PER_EVENT. */
159
+ tags?: Record<string, string> | null;
160
+ /** Free-form custom context the customer sets (`setContext`): the
161
+ * application state that makes an issue diagnosable and that no schema of
162
+ * ours could have anticipated. Values pass the pattern scrub. At most
163
+ * MAX_CONTEXT_ENTRIES. */
164
+ context?: Record<string, string> | null;
165
+ /** Runtime/platform metadata the SDK reads from its own process. */
166
+ device?: WireDevice | null;
167
+ /** REA-257: a client-minted UUID for this event, present ONLY when the
168
+ * integration opted into user feedback (`init({ feedback: {...} })`). It
169
+ * is what a later `POST /api/errors/v1/feedback/<key>` names to attach a
170
+ * user's report to this exact occurrence. An integration that never
171
+ * enables feedback keeps emitting the id-less payload it always did. */
172
+ eventId?: string;
173
+ }
174
+ export interface WireBatch {
175
+ /** e.g. "realuptime-errors-js/0.1.0" */
176
+ sdk: string;
177
+ /** Events this SDK dropped client-side (bounded buffer overflow) since the
178
+ * last successful delivery. Relayed so even client-side drops reach the
179
+ * product's visible drop counters -- never omitted, never reset without
180
+ * being reported. */
181
+ droppedClient: number;
182
+ events: WireEvent[];
183
+ }
184
+ /** Success body. `accepted` + `droppedOverQuota` always sum to the batch's
185
+ * event count; `overQuota: true` with `resetsAt` tells the SDK to stop
186
+ * sending until the window resets rather than burning the customer's cycles
187
+ * on refusals. */
188
+ export interface WireIngestResult {
189
+ accepted: number;
190
+ droppedOverQuota: number;
191
+ overQuota: boolean;
192
+ quota: {
193
+ used: number;
194
+ limit: number;
195
+ resetsAt: string;
196
+ };
197
+ }
198
+ /** Machine-readable refusal reasons the SDK understands. Anything else is
199
+ * treated as transient.
200
+ *
201
+ * `oversize_context` (v2, RU-4009) is a 400 like `malformed`, and both SDKs
202
+ * already handle every 400 the same way -- drop the batch, log once, never
203
+ * retry -- so it needs no new client branch. It is a SEPARATE reason string
204
+ * because "your event carried 500 tags" and "your event was not valid JSON"
205
+ * are different bugs in the customer's integration, and telling them apart
206
+ * costs one string. */
207
+ export type WireRefusalReason = "over_quota" | "rate_limited" | "key_revoked" | "malformed" | "oversize_context";
208
+ export declare const MAX_EVENTS_PER_BATCH = 50;
209
+ export declare const MAX_MESSAGE_LENGTH = 4000;
210
+ export declare const MAX_FRAMES_PER_EVENT = 50;
211
+ export declare const MAX_STRING_LENGTH = 512;
212
+ export declare const MAX_BREADCRUMBS_PER_EVENT = 20;
213
+ export declare const MAX_BREADCRUMB_DATA_ENTRIES = 10;
214
+ /** Tags per event kept; extras are dropped and counted. */
215
+ export declare const MAX_TAGS_PER_EVENT = 20;
216
+ /** Custom-context entries per event kept; extras are dropped and counted. */
217
+ export declare const MAX_CONTEXT_ENTRIES = 20;
218
+ /** Tag and context KEY length. Shorter than MAX_STRING_LENGTH because a key
219
+ * is a label, not a payload; a 512-character key is a value in disguise. */
220
+ export declare const MAX_CONTEXT_KEY_LENGTH = 64;
221
+ /** Local variables kept per frame when the (off-by-default) capture is on. */
222
+ export declare const MAX_LOCAL_VARS_PER_FRAME = 20;
223
+ /** Per-value cap for a local variable, tighter than MAX_STRING_LENGTH: a
224
+ * local is captured for its shape, not its contents. */
225
+ export declare const MAX_LOCAL_VAR_LENGTH = 256;
226
+ /** Combined serialized budget for user + tags + context + device on ONE
227
+ * event, enforced server-side inside the ingest transaction. Entries past
228
+ * it are dropped and counted, same honesty rule as the soft caps. */
229
+ export declare const MAX_CONTEXT_BYTES = 4096;
230
+ /** The refusal boundary: more entries than this in any one map is a bug in
231
+ * the caller, not an over-eager integration. */
232
+ export declare const HARD_MAX_MAP_ENTRIES = 200;
233
+ /** `POST /api/errors/v1/feedback/<key>` body. */
234
+ export interface WireFeedback {
235
+ /** The client-minted event id the report attaches to (WireEvent.eventId). */
236
+ eventId: string;
237
+ /** What the user typed. Scrubbed client-side before serialization. */
238
+ comments: string;
239
+ /** Optional, only sent when the dialog showed the field and the user
240
+ * filled it. Scrubbed by the same pattern rules. */
241
+ name?: string;
242
+ email?: string;
243
+ /** e.g. "realuptime-errors-js/0.4.0" */
244
+ sdk: string;
245
+ }
246
+ export declare const MAX_FEEDBACK_COMMENTS_LENGTH = 4000;
247
+ export declare const MAX_FEEDBACK_NAME_LENGTH = 200;
248
+ export declare const MAX_FEEDBACK_EMAIL_LENGTH = 254;
249
+ /** Body size ceiling the server enforces on the feedback endpoint. */
250
+ export declare const MAX_FEEDBACK_BODY_BYTES = 16384;
package/dist/types.js ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The Errors wire contract (docs/errors-plan.md, "The SDKs: two
3
+ * languages, one contract").
4
+ *
5
+ * DUPLICATED, not imported. Same convention as apps/agent/types.ts and for
6
+ * the same reason: this package ships into customer bundles and processes,
7
+ * so its dependency graph must stay auditable at a glance -- zero imports
8
+ * beyond the platform. The cost (a server-side rename does not break this
9
+ * build) is paid by wire-contract.test.ts, which pins every field name, and
10
+ * by the shared scrub vectors (scrub-vectors.json), which pin the scrub
11
+ * semantics across this SDK, the Python SDK, and the server's second net.
12
+ * The server-side mirror of these limits lives in
13
+ * packages/db/error-ingest.ts.
14
+ *
15
+ * ## v2 (REA-182): identity, tags, custom context, device metadata
16
+ *
17
+ * STRICTLY ADDITIVE, and that word is load-bearing rather than decorative.
18
+ * Every v1 field keeps its name, its type and its meaning; every v2 field is
19
+ * OPTIONAL and absent from a v1 payload. A v1 batch produced by an SDK that
20
+ * predates this file still ingests byte-for-byte unchanged, which
21
+ * route.test.ts pins with a frozen golden v1 body.
22
+ *
23
+ * **There is deliberately no `version` field on the wire, and that is a
24
+ * decision, not an omission.** A version discriminant would have to be
25
+ * REQUIRED to be worth reading, and a required field is by construction not
26
+ * additive: every already-deployed SDK in a customer's process omits it, so
27
+ * the server would have to treat "absent" as v1 anyway -- which is exactly
28
+ * what optional-field presence already tells it, with no field to keep in
29
+ * sync. The `sdk` string ("realuptime-errors-js/0.3.0") remains the honest
30
+ * record of what produced a payload. A future change that is NOT additive
31
+ * (a removed field, a changed meaning) gets a versioned PATH, the way the
32
+ * fingerprint algorithm gets a version rather than mutating history.
33
+ */
34
+ // Mirrored by MAX_ERROR_EVENTS_PER_BATCH etc. in packages/db/error-ingest.ts
35
+ // and pinned by wire-contract.test.ts on both sides.
36
+ export const MAX_EVENTS_PER_BATCH = 50;
37
+ export const MAX_MESSAGE_LENGTH = 4000;
38
+ export const MAX_FRAMES_PER_EVENT = 50;
39
+ export const MAX_STRING_LENGTH = 512;
40
+ export const MAX_BREADCRUMBS_PER_EVENT = 20;
41
+ export const MAX_BREADCRUMB_DATA_ENTRIES = 10;
42
+ // ── v2 (REA-182) caps ───────────────────────────────────────────────────
43
+ //
44
+ // Two tiers, deliberately:
45
+ //
46
+ // SOFT caps (the constants below) are what an honest integration lives
47
+ // inside. Going past one is not an error: the server keeps the first N
48
+ // entries, COUNTS what it dropped, and stores the count so the issue
49
+ // detail can say "3 context entries were dropped (size cap)". Truncating
50
+ // silently is the exact lie this product was founded against, so the
51
+ // count is first-class data rather than a log line.
52
+ //
53
+ // The HARD bound (HARD_MAX_MAP_ENTRIES) is where a payload stops being an
54
+ // over-eager integration and starts being a bug or an attack; that one
55
+ // REFUSES the batch with RU-4009 rather than quietly keeping 20 of 5,000.
56
+ /** Tags per event kept; extras are dropped and counted. */
57
+ export const MAX_TAGS_PER_EVENT = 20;
58
+ /** Custom-context entries per event kept; extras are dropped and counted. */
59
+ export const MAX_CONTEXT_ENTRIES = 20;
60
+ /** Tag and context KEY length. Shorter than MAX_STRING_LENGTH because a key
61
+ * is a label, not a payload; a 512-character key is a value in disguise. */
62
+ export const MAX_CONTEXT_KEY_LENGTH = 64;
63
+ /** Local variables kept per frame when the (off-by-default) capture is on. */
64
+ export const MAX_LOCAL_VARS_PER_FRAME = 20;
65
+ /** Per-value cap for a local variable, tighter than MAX_STRING_LENGTH: a
66
+ * local is captured for its shape, not its contents. */
67
+ export const MAX_LOCAL_VAR_LENGTH = 256;
68
+ /** Combined serialized budget for user + tags + context + device on ONE
69
+ * event, enforced server-side inside the ingest transaction. Entries past
70
+ * it are dropped and counted, same honesty rule as the soft caps. */
71
+ export const MAX_CONTEXT_BYTES = 4096;
72
+ /** The refusal boundary: more entries than this in any one map is a bug in
73
+ * the caller, not an over-eager integration. */
74
+ export const HARD_MAX_MAP_ENTRIES = 200;
75
+ export const MAX_FEEDBACK_COMMENTS_LENGTH = 4000;
76
+ export const MAX_FEEDBACK_NAME_LENGTH = 200;
77
+ export const MAX_FEEDBACK_EMAIL_LENGTH = 254;
78
+ /** Body size ceiling the server enforces on the feedback endpoint. */
79
+ export const MAX_FEEDBACK_BODY_BYTES = 16_384;
package/dist/vite.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { type SourcemapFile, type UploadSummary } from "./sourcemaps.ts";
2
+ /**
3
+ * Vite plugin (REA-254): uploads the `.map` files a production build wrote
4
+ * to its outDir, on `closeBundle`, to the project's source-map endpoint.
5
+ *
6
+ * // vite.config.ts
7
+ * import { realuptimeSourcemaps } from "@realuptime/errors/vite";
8
+ * export default defineConfig({
9
+ * build: { sourcemap: true },
10
+ * plugins: [
11
+ * realuptimeSourcemaps({
12
+ * release: process.env.RELEASE ?? process.env.GITHUB_SHA!,
13
+ * dsn: process.env.REALUPTIME_ERRORS_DSN,
14
+ * }),
15
+ * ],
16
+ * });
17
+ *
18
+ * It does not import "vite": the hook shapes are matched structurally, so
19
+ * this stays a zero-dependency file in a zero-dependency package (the same
20
+ * rule the framework adapters follow). It never changes the build output:
21
+ * no map stripping, no injected code, no release constant, nothing. It reads
22
+ * the maps Vite already wrote and posts them.
23
+ *
24
+ * Failure policy: an upload failure is logged loudly with the server's own
25
+ * message and the build still succeeds, because a deploy pipeline that
26
+ * cannot reach realuptime should not also fail to ship; set
27
+ * `failOnError: true` to make the build fail instead. Either way the
28
+ * failure is one clear line, never silence.
29
+ */
30
+ export interface SourcemapPluginOptions {
31
+ /** The release string the app reports at runtime (`init({ release })`).
32
+ * The maps are matched to events by this exact string. */
33
+ release: string;
34
+ /** The project DSN; `REALUPTIME_ERRORS_DSN` is read when omitted. */
35
+ dsn?: string;
36
+ /** A bare ingest key (`REALUPTIME_ERRORS_INGEST_KEY` when omitted), used
37
+ * with `origin` when no DSN is set. */
38
+ ingestKey?: string;
39
+ origin?: string;
40
+ /** Prefix artifact names with the public path the bundle is served from
41
+ * (e.g. `~/assets` or `static/js`), when the outDir layout does not match
42
+ * the URL path. Matching is by path suffix, so it is rarely needed. */
43
+ urlPrefix?: string;
44
+ /** Override the directory to collect from; defaults to the resolved
45
+ * build outDir. */
46
+ dir?: string;
47
+ /** Skip the upload without failing (e.g. local builds): when false, the
48
+ * plugin logs one line and does nothing. Default: upload whenever a
49
+ * credential is present. */
50
+ enabled?: boolean;
51
+ failOnError?: boolean;
52
+ /** Test seams. */
53
+ fetchImpl?: typeof fetch;
54
+ log?: (line: string) => void;
55
+ env?: Record<string, string | undefined>;
56
+ }
57
+ /** The slice of Vite's ResolvedConfig this plugin reads. */
58
+ interface ResolvedConfigLike {
59
+ root?: string;
60
+ build?: {
61
+ outDir?: string;
62
+ };
63
+ }
64
+ export interface SourcemapPlugin {
65
+ name: string;
66
+ apply: "build";
67
+ enforce: "post";
68
+ configResolved(config: ResolvedConfigLike): void;
69
+ closeBundle(): Promise<void>;
70
+ }
71
+ /** The shared run both plugins call: collect, pre-flight, upload, report.
72
+ * Returns the summary on success, null when skipped or failed (already
73
+ * logged); throws only when `failOnError` is set. */
74
+ export declare function runSourcemapUpload(options: SourcemapPluginOptions, dir: string, label: string): Promise<UploadSummary | null>;
75
+ export declare function realuptimeSourcemaps(options: SourcemapPluginOptions): SourcemapPlugin;
76
+ export type { SourcemapFile, UploadSummary };
package/dist/vite.js ADDED
@@ -0,0 +1,75 @@
1
+ import { resolve } from "node:path";
2
+ import { collectSourcemapFiles, formatProgressLine, preflightSourcemapFiles, resolveSourcemapUploadUrl, uploadSourcemaps, } from "./sourcemaps.js";
3
+ /** The shared run both plugins call: collect, pre-flight, upload, report.
4
+ * Returns the summary on success, null when skipped or failed (already
5
+ * logged); throws only when `failOnError` is set. */
6
+ export async function runSourcemapUpload(options, dir, label) {
7
+ const log = options.log ?? ((line) => console.error(line));
8
+ const env = options.env ?? process.env;
9
+ const prefix = `[realuptime-errors ${label}]`;
10
+ const fail = (message) => {
11
+ if (options.failOnError)
12
+ throw new Error(`${prefix} ${message}`);
13
+ log(`${prefix} ${message}`);
14
+ return null;
15
+ };
16
+ if (options.enabled === false) {
17
+ log(`${prefix} disabled; no source maps uploaded.`);
18
+ return null;
19
+ }
20
+ const dsn = options.dsn ?? env.REALUPTIME_ERRORS_DSN;
21
+ const ingestKey = options.ingestKey ?? env.REALUPTIME_ERRORS_INGEST_KEY;
22
+ if (!dsn && !ingestKey) {
23
+ log(`${prefix} no REALUPTIME_ERRORS_DSN or ingest key set; no source maps uploaded for ${options.release}.`);
24
+ return null;
25
+ }
26
+ if (!options.release)
27
+ return fail("release is required; no source maps uploaded.");
28
+ let url;
29
+ try {
30
+ url = resolveSourcemapUploadUrl({ dsn, ingestKey, origin: options.origin });
31
+ }
32
+ catch (cause) {
33
+ return fail(cause instanceof Error ? cause.message : String(cause));
34
+ }
35
+ const { files, duplicates, missing } = collectSourcemapFiles([dir], { urlPrefix: options.urlPrefix });
36
+ if (missing.length > 0)
37
+ return fail(`${dir} does not exist; no source maps uploaded.`);
38
+ const problem = preflightSourcemapFiles(files);
39
+ if (problem)
40
+ return fail(problem);
41
+ for (const name of duplicates)
42
+ log(`${prefix} duplicate artifact name ${name}; the first occurrence was kept.`);
43
+ try {
44
+ const summary = await uploadSourcemaps({
45
+ url,
46
+ release: options.release,
47
+ files,
48
+ fetchImpl: options.fetchImpl,
49
+ onProgress: (progress) => log(`${prefix} ${formatProgressLine(progress)}`),
50
+ });
51
+ log(`${prefix} uploaded ${summary.uploaded} source map${summary.uploaded === 1 ? "" : "s"} for release ${summary.release} (stored ${summary.stored}, replaced ${summary.replaced}).`);
52
+ return summary;
53
+ }
54
+ catch (cause) {
55
+ return fail(cause instanceof Error ? cause.message : String(cause));
56
+ }
57
+ }
58
+ export function realuptimeSourcemaps(options) {
59
+ let outDir = options.dir ?? "dist";
60
+ return {
61
+ name: "realuptime-errors-sourcemaps",
62
+ apply: "build",
63
+ enforce: "post",
64
+ configResolved(config) {
65
+ if (options.dir) {
66
+ outDir = resolve(config.root ?? process.cwd(), options.dir);
67
+ return;
68
+ }
69
+ outDir = resolve(config.root ?? process.cwd(), config.build?.outDir ?? "dist");
70
+ },
71
+ async closeBundle() {
72
+ await runSourcemapUpload(options, outDir, "vite");
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,38 @@
1
+ import { type SourcemapPluginOptions } from "./vite.ts";
2
+ /**
3
+ * webpack plugin (REA-254), same core as the Vite plugin: after webpack
4
+ * emits its assets, the `.map` files in `output.path` are uploaded to the
5
+ * project's source-map endpoint for the given release.
6
+ *
7
+ * // webpack.config.js
8
+ * const { RealuptimeSourcemapsPlugin } = require("@realuptime/errors/webpack");
9
+ * module.exports = {
10
+ * devtool: "source-map",
11
+ * plugins: [new RealuptimeSourcemapsPlugin({ release: process.env.GITHUB_SHA })],
12
+ * };
13
+ *
14
+ * Works with webpack 4 and 5 and with Rspack: it only uses
15
+ * `compiler.hooks.afterEmit.tapPromise` and `compiler.options.output.path`,
16
+ * both matched structurally (no "webpack" import, zero dependencies). It
17
+ * never changes the emitted assets. Failure policy is the Vite plugin's:
18
+ * loud log, build still succeeds, unless `failOnError: true`.
19
+ */
20
+ interface CompilerLike {
21
+ options?: {
22
+ output?: {
23
+ path?: string;
24
+ };
25
+ context?: string;
26
+ };
27
+ hooks: {
28
+ afterEmit: {
29
+ tapPromise(name: string, fn: () => Promise<void>): void;
30
+ };
31
+ };
32
+ }
33
+ export declare class RealuptimeSourcemapsPlugin {
34
+ private readonly options;
35
+ constructor(options: SourcemapPluginOptions);
36
+ apply(compiler: CompilerLike): void;
37
+ }
38
+ export type { SourcemapPluginOptions };
@@ -0,0 +1,15 @@
1
+ import { resolve } from "node:path";
2
+ import { runSourcemapUpload } from "./vite.js";
3
+ export class RealuptimeSourcemapsPlugin {
4
+ options;
5
+ constructor(options) {
6
+ this.options = options;
7
+ }
8
+ apply(compiler) {
9
+ compiler.hooks.afterEmit.tapPromise("RealuptimeSourcemapsPlugin", async () => {
10
+ const context = compiler.options?.context ?? process.cwd();
11
+ const dir = this.options.dir ? resolve(context, this.options.dir) : (compiler.options?.output?.path ?? resolve(context, "dist"));
12
+ await runSourcemapUpload(this.options, dir, "webpack");
13
+ });
14
+ }
15
+ }