@flareapp/svelte 2.0.0-alpha.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 ADDED
@@ -0,0 +1,365 @@
1
+ # @flareapp/svelte
2
+
3
+ Svelte 5 integration for [Flare](https://flareapp.io) error tracking. It provides a native Svelte error boundary and a
4
+ handler factory for custom `<svelte:boundary>` usage. It builds on top of `@flareapp/js`, which still handles the core
5
+ Flare client configuration, global browser errors, manual reports, custom context, and glows.
6
+
7
+ ## Installation
8
+
9
+ Install both the core Flare client and the Svelte integration:
10
+
11
+ ```bash
12
+ npm install @flareapp/js @flareapp/svelte
13
+ # or
14
+ yarn add @flareapp/js @flareapp/svelte
15
+ # or
16
+ pnpm add @flareapp/js @flareapp/svelte
17
+ ```
18
+
19
+ `@flareapp/svelte` supports Svelte 5.3 and higher.
20
+
21
+ If your app is bundled for production, also configure sourcemap uploads with `@flareapp/vite` so Flare can show readable
22
+ stack traces and code snippets.
23
+
24
+ ## Setting up the Flare client
25
+
26
+ Initialize the Flare client as early as possible in your application, typically in `main.ts`:
27
+
28
+ ```ts
29
+ import { flare } from '@flareapp/js';
30
+ import { mount } from 'svelte';
31
+
32
+ import App from './App.svelte';
33
+
34
+ if (import.meta.env.PROD) {
35
+ flare.light('YOUR PROJECT PUBLIC KEY');
36
+ }
37
+
38
+ mount(App, {
39
+ target: document.getElementById('app')!,
40
+ });
41
+ ```
42
+
43
+ Use your project's public key from the JavaScript installation section in your Flare project settings.
44
+
45
+ If you use the sourcemap plugin, you do not need to pass a public key to `flare.light()`. The plugin injects the project
46
+ key during your build.
47
+
48
+ ## Error boundary
49
+
50
+ `FlareErrorBoundary` wraps Svelte's native `<svelte:boundary>`. It catches errors from the component tree below it,
51
+ reports them to Flare, and can render your fallback snippet.
52
+
53
+ ```svelte
54
+ <script lang="ts">
55
+ import { FlareErrorBoundary } from '@flareapp/svelte';
56
+
57
+ import Root from './Root.svelte';
58
+ </script>
59
+
60
+ <FlareErrorBoundary>
61
+ <Root />
62
+
63
+ {#snippet failed(error, reset)}
64
+ <section>
65
+ <h2>Something went wrong</h2>
66
+ <p>{error.message}</p>
67
+ <button onclick={reset}>Try again</button>
68
+ </section>
69
+ {/snippet}
70
+ </FlareErrorBoundary>
71
+ ```
72
+
73
+ Without a `failed` snippet, the boundary still catches and reports the error, but it renders nothing for the failed
74
+ subtree.
75
+
76
+ ### Resetting the boundary
77
+
78
+ Call the `reset` function passed to the `failed` snippet to clear the boundary state and retry rendering the children:
79
+
80
+ ```svelte
81
+ <FlareErrorBoundary>
82
+ <CheckoutForm />
83
+
84
+ {#snippet failed(error, reset)}
85
+ <p>{error.message}</p>
86
+ <button onclick={reset}>Retry checkout</button>
87
+ {/snippet}
88
+ </FlareErrorBoundary>
89
+ ```
90
+
91
+ You can also reset automatically when values in `resetKeys` change. Values are compared by index with `Object.is`, and a
92
+ length change also triggers a reset.
93
+
94
+ ```svelte
95
+ <FlareErrorBoundary
96
+ resetKeys={[currentRoute, selectedAccountId]}
97
+ onReset={(error) => {
98
+ console.log('Recovered from:', error?.message);
99
+ }}
100
+ >
101
+ <AccountPage />
102
+
103
+ {#snippet failed(error, reset)}
104
+ <p>{error.message}</p>
105
+ <button onclick={reset}>Try again</button>
106
+ {/snippet}
107
+ </FlareErrorBoundary>
108
+ ```
109
+
110
+ `onReset` runs when the user calls `reset` from the fallback snippet or when `resetKeys` changes while the boundary is in
111
+ an error state. It receives the previous error, or `null` if no error was stored.
112
+
113
+ ## Lifecycle callbacks
114
+
115
+ The boundary exposes three callbacks around the Svelte-specific reporting flow.
116
+
117
+ ```svelte
118
+ <script lang="ts">
119
+ import { flare } from '@flareapp/js';
120
+ import { FlareErrorBoundary, type FlareSvelteContext } from '@flareapp/svelte';
121
+ </script>
122
+
123
+ <FlareErrorBoundary
124
+ beforeEvaluate={({ error }) => {
125
+ flare.addContext('feature', 'checkout');
126
+ flare.addContext('errorMessage', error.message);
127
+ }}
128
+ beforeSubmit={({ context }: { context: FlareSvelteContext }) => {
129
+ return {
130
+ ...context,
131
+ svelte: {
132
+ ...context.svelte,
133
+ componentHierarchy: context.svelte.componentHierarchy.filter(
134
+ (component) => component !== 'ThirdPartyWrapper',
135
+ ),
136
+ },
137
+ };
138
+ }}
139
+ afterSubmit={({ error, context }) => {
140
+ console.error('Reported Svelte error:', error);
141
+ console.debug('Svelte context:', context);
142
+ }}
143
+ >
144
+ <Root />
145
+ </FlareErrorBoundary>
146
+ ```
147
+
148
+ Callback order:
149
+
150
+ 1. `beforeEvaluate` runs after the thrown value is converted to an `Error`, before Svelte context is built.
151
+ 2. `beforeSubmit` runs after Svelte context is built. Return the context object that should be attached to the report.
152
+ 3. `afterSubmit` runs after Flare reporting is started. The network request is asynchronous.
153
+
154
+ These callbacks are not wrapped in `try`/`catch`. If one throws, the error can bubble out of the boundary handler.
155
+
156
+ ### Filtering errors
157
+
158
+ The boundary callbacks are for adding context and running side effects. They do not suppress reports.
159
+
160
+ To filter, suppress, or modify the final Flare report, use the core JavaScript client hooks:
161
+
162
+ ```ts
163
+ import { flare } from '@flareapp/js';
164
+
165
+ flare.configure({
166
+ beforeEvaluate: (error) => {
167
+ if (error.message.includes('Ignored validation error')) {
168
+ return false;
169
+ }
170
+
171
+ return error;
172
+ },
173
+ });
174
+ ```
175
+
176
+ The execution order when both boundary callbacks and core client hooks are configured is:
177
+
178
+ 1. Boundary `beforeEvaluate`
179
+ 2. Boundary `beforeSubmit`
180
+ 3. Internal `flare.reportSilently()` call
181
+ 4. Client `beforeEvaluate` from `flare.configure()`
182
+ 5. Client `beforeSubmit` from `flare.configure()`
183
+ 6. Report is sent to Flare
184
+ 7. Boundary `afterSubmit`
185
+
186
+ ## Svelte context
187
+
188
+ When the Svelte integration reports an error, it attaches Svelte-specific context under `context.custom.svelte`.
189
+
190
+ ```ts
191
+ interface FlareSvelteContext {
192
+ svelte: {
193
+ componentName: string | null;
194
+ componentHierarchy: string[];
195
+ errorOrigin: 'render' | 'event' | 'effect' | 'unknown';
196
+ };
197
+ }
198
+ ```
199
+
200
+ Field details:
201
+
202
+ | Field | Description |
203
+ | -------------------- | ---------------------------------------------------------------- |
204
+ | `componentName` | Best-effort name of the component closest to the thrown error. |
205
+ | `componentHierarchy` | Component names ordered from inner component to outer component. |
206
+ | `errorOrigin` | Best-effort classification of where the error came from. |
207
+
208
+ When the same component is mounted in multiple places (e.g. a `Button` inside both `Sidebar` and `Header`),
209
+ `FlareErrorBoundary` disambiguates by matching the error to the instance whose ancestor chain includes the catching
210
+ boundary. Without an ancestor hint (e.g. manual `lookupComponentTree` calls), the first registered instance is returned.
211
+
212
+ Component context is extracted from `.svelte` stack frames. In production bundles, function names and filenames may be
213
+ minified. Configure sourcemaps so Flare can resolve the original source code on the backend.
214
+
215
+ ## Event handlers and async errors
216
+
217
+ Svelte boundaries do not catch every kind of browser error. Errors thrown in event handlers and unhandled promise
218
+ rejections are handled by the global listeners installed by `@flareapp/js`, not by `FlareErrorBoundary`.
219
+
220
+ ```svelte
221
+ <button
222
+ onclick={() => {
223
+ throw new Error('Clicked button failed');
224
+ }}
225
+ >
226
+ Trigger event error
227
+ </button>
228
+ ```
229
+
230
+ The error above is still reported if the core Flare client is initialized, but it will not render the boundary fallback
231
+ UI.
232
+
233
+ ## Custom boundary usage
234
+
235
+ Use `createFlareErrorHandler()` when you want to wire Flare into your own `<svelte:boundary>` instead of using
236
+ `FlareErrorBoundary`.
237
+
238
+ ```svelte
239
+ <script lang="ts">
240
+ import { createFlareErrorHandler } from '@flareapp/svelte';
241
+
242
+ const reportToFlare = createFlareErrorHandler({
243
+ afterSubmit: ({ error }) => {
244
+ console.error('Reported through custom boundary:', error);
245
+ },
246
+ });
247
+ </script>
248
+
249
+ <svelte:boundary onerror={reportToFlare}>
250
+ <Root />
251
+
252
+ {#snippet failed(error, reset)}
253
+ <p>{error.message}</p>
254
+ <button onclick={reset}>Retry</button>
255
+ {/snippet}
256
+ </svelte:boundary>
257
+ ```
258
+
259
+ The returned function matches the Svelte boundary `onerror` signature:
260
+
261
+ ```ts
262
+ (error: unknown, reset: () => void) => void | Promise<void>;
263
+ ```
264
+
265
+ It converts non-`Error` values, builds Svelte context from the stack trace, reports through the core Flare client, and
266
+ runs the lifecycle callbacks described above.
267
+
268
+ ## Manual reports, context, and glows
269
+
270
+ The Svelte integration builds on the core JavaScript client. Use `@flareapp/js` directly for manual reporting, client
271
+ hooks, custom context, and glows:
272
+
273
+ ```ts
274
+ import { flare } from '@flareapp/js';
275
+
276
+ flare.addContext('user', { id: '123' });
277
+ flare.glow('checkout', 'Payment method selected');
278
+
279
+ try {
280
+ await submitOrder();
281
+ } catch (error) {
282
+ flare.report(error);
283
+ }
284
+ ```
285
+
286
+ Useful shared documentation:
287
+
288
+ - [Reporting errors](https://flareapp.io/docs/javascript/errors/reporting-errors)
289
+ - [Client hooks](https://flareapp.io/docs/javascript/errors/client-hooks)
290
+ - [Adding custom context](https://flareapp.io/docs/javascript/data-collection/adding-custom-context)
291
+ - [Adding glows](https://flareapp.io/docs/javascript/data-collection/adding-glows)
292
+
293
+ ## Resolving bundled code
294
+
295
+ Production Svelte apps are usually minified and bundled, which makes raw stack traces hard to read. Configure sourcemap
296
+ uploads with `@flareapp/vite` so Flare can map stack frames back to your original `.svelte` files.
297
+
298
+ The Svelte integration uses the same sourcemap plugin as the JavaScript and React clients. See the
299
+ [JavaScript resolving bundled code documentation](https://flareapp.io/docs/javascript/general/resolving-bundled-code)
300
+ for the Vite, Webpack, Laravel Mix, and manual upload setup.
301
+
302
+ ## Verifying your setup
303
+
304
+ The core client is available as `window.flare` in the browser. Build your app for production and run this in the browser
305
+ console:
306
+
307
+ ```js
308
+ flare.test();
309
+ ```
310
+
311
+ This sends a test error to your Flare project.
312
+
313
+ If nothing appears in Flare, enable debug mode:
314
+
315
+ ```ts
316
+ flare.light('YOUR PROJECT PUBLIC KEY', true);
317
+ // or
318
+ flare.configure({ debug: true });
319
+ ```
320
+
321
+ If `flare.light()` has not been called, for example because your production guard is false, reports are silently ignored.
322
+
323
+ ## API reference
324
+
325
+ ```ts
326
+ import { FlareErrorBoundary, createFlareErrorHandler } from '@flareapp/svelte';
327
+
328
+ import type { FlareErrorHandlerOptions, FlareSvelteContext, SvelteErrorOrigin } from '@flareapp/svelte';
329
+ ```
330
+
331
+ Exports:
332
+
333
+ | Export | Description |
334
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------- |
335
+ | `FlareErrorBoundary` | Svelte component that catches boundary errors, reports them to Flare, and renders an optional fallback snippet. |
336
+ | `createFlareErrorHandler` | Factory that returns a Svelte boundary `onerror` callback for custom boundary usage. |
337
+
338
+ Types:
339
+
340
+ | Type | Description |
341
+ | -------------------------- | ------------------------------------------------------------------------------------------ |
342
+ | `FlareErrorHandlerOptions` | Lifecycle callback options accepted by `createFlareErrorHandler` and `FlareErrorBoundary`. |
343
+ | `FlareSvelteContext` | Shape of the Svelte context passed to `beforeSubmit` and `afterSubmit`. |
344
+ | `SvelteErrorOrigin` | Union of possible origin values: `'render'`, `'event'`, `'effect'`, and `'unknown'`. |
345
+
346
+ ### `FlareErrorBoundary` props
347
+
348
+ | Prop | Type | Description |
349
+ | ---------------- | -------------------------------------------- | --------------------------------------------------------------------- |
350
+ | `children` | `Snippet` | Child snippet rendered inside the boundary. |
351
+ | `failed` | `Snippet<[error: Error, reset: () => void]>` | Optional fallback snippet rendered after an error is caught. |
352
+ | `resetKeys` | `unknown[]` | Values that reset the boundary when changed while an error is stored. |
353
+ | `beforeEvaluate` | `({ error }) => void` | Runs before Svelte context is built. |
354
+ | `beforeSubmit` | `({ error, context }) => FlareSvelteContext` | Runs before reporting. Return the context to attach. |
355
+ | `afterSubmit` | `({ error, context }) => void` | Runs after reporting is started. |
356
+ | `onReset` | `(error: Error &#124; null) => void` | Runs when the boundary is reset. |
357
+
358
+ ## SvelteKit
359
+
360
+ For SvelteKit apps, install `@flareapp/sveltekit` as well. It adds client and server `handleError` helpers, manual
361
+ SvelteKit capture helpers, route context, and re-exports the Svelte boundary component for convenience.
362
+
363
+ ## License
364
+
365
+ The MIT License (MIT). Please see [License File](../../LICENSE.md) for more information.
@@ -0,0 +1,78 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ import { __flareRegisterComponent, getComponentTreeContext } from './componentTree.js';
5
+ import { createFlareErrorHandler, type FlareErrorHandlerOptions } from './createFlareErrorHandler.js';
6
+
7
+ interface Props {
8
+ children: Snippet;
9
+ failed?: Snippet<[error: Error, reset: () => void]>;
10
+ resetKeys?: unknown[];
11
+ beforeEvaluate?: FlareErrorHandlerOptions['beforeEvaluate'];
12
+ beforeSubmit?: FlareErrorHandlerOptions['beforeSubmit'];
13
+ afterSubmit?: FlareErrorHandlerOptions['afterSubmit'];
14
+ onReset?: (error: Error | null) => void;
15
+ }
16
+
17
+ let {
18
+ children,
19
+ failed: fallbackSnippet,
20
+ resetKeys,
21
+ beforeEvaluate,
22
+ beforeSubmit,
23
+ afterSubmit,
24
+ onReset,
25
+ }: Props = $props();
26
+
27
+ let currentError: Error | null = $state(null);
28
+ let resetBoundary: (() => void) | null = $state(null);
29
+
30
+ let previousKeys: unknown[] | undefined;
31
+ $effect(() => {
32
+ if (!currentError || !resetKeys) {
33
+ previousKeys = resetKeys ? [...resetKeys] : undefined;
34
+ return;
35
+ }
36
+
37
+ const lengthChanged = previousKeys?.length !== resetKeys.length;
38
+ const valuesChanged = resetKeys.some((key, i) => !Object.is(key, previousKeys?.[i]));
39
+
40
+ if (lengthChanged || valuesChanged) {
41
+ handleReset();
42
+ }
43
+
44
+ previousKeys = [...resetKeys];
45
+ });
46
+
47
+ function handleReset() {
48
+ const error = currentError;
49
+ currentError = null;
50
+ onReset?.(error);
51
+ resetBoundary?.();
52
+ resetBoundary = null;
53
+ }
54
+
55
+ const ancestor = __flareRegisterComponent('FlareErrorBoundary', '@flareapp/svelte/FlareErrorBoundary.svelte');
56
+
57
+ const handler = $derived(
58
+ createFlareErrorHandler({ ancestor, beforeEvaluate, beforeSubmit, afterSubmit }),
59
+ );
60
+
61
+ function onerror(rawError: unknown, reset: () => void) {
62
+ resetBoundary = reset;
63
+ const error = rawError instanceof Error ? rawError : new Error(String(rawError));
64
+ currentError = error;
65
+
66
+ handler(rawError, reset);
67
+ }
68
+ </script>
69
+
70
+ <svelte:boundary {onerror}>
71
+ {@render children()}
72
+
73
+ {#snippet failed(error, reset)}
74
+ {#if fallbackSnippet}
75
+ {@render fallbackSnippet(error instanceof Error ? error : new Error(String(error)), handleReset)}
76
+ {/if}
77
+ {/snippet}
78
+ </svelte:boundary>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type FlareErrorHandlerOptions } from './createFlareErrorHandler.js';
3
+ interface Props {
4
+ children: Snippet;
5
+ failed?: Snippet<[error: Error, reset: () => void]>;
6
+ resetKeys?: unknown[];
7
+ beforeEvaluate?: FlareErrorHandlerOptions['beforeEvaluate'];
8
+ beforeSubmit?: FlareErrorHandlerOptions['beforeSubmit'];
9
+ afterSubmit?: FlareErrorHandlerOptions['afterSubmit'];
10
+ onReset?: (error: Error | null) => void;
11
+ }
12
+ declare const FlareErrorBoundary: import("svelte").Component<Props, {}, "">;
13
+ type FlareErrorBoundary = ReturnType<typeof FlareErrorBoundary>;
14
+ export default FlareErrorBoundary;
@@ -0,0 +1,9 @@
1
+ export interface ComponentTreeNode {
2
+ name: string;
3
+ file: string;
4
+ parent: ComponentTreeNode | null;
5
+ }
6
+ export declare function __flareRegisterComponent(name: string, file: string): ComponentTreeNode;
7
+ export declare function getComponentTreeContext(): ComponentTreeNode | null;
8
+ export declare function lookupComponentTree(fileName: string, ancestor?: ComponentTreeNode | null): string[];
9
+ export declare function findNode(fileName: string, ancestor?: ComponentTreeNode | null): ComponentTreeNode | undefined;
@@ -0,0 +1,99 @@
1
+ import { getContext, onDestroy, setContext } from 'svelte';
2
+ const CONTEXT_KEY = '__flare_component_tree';
3
+ const registry = new Map();
4
+ export function __flareRegisterComponent(name, file) {
5
+ let parent = null;
6
+ try {
7
+ parent = getContext(CONTEXT_KEY) ?? null;
8
+ }
9
+ catch {
10
+ // getContext throws outside component init
11
+ }
12
+ const node = { name, file, parent };
13
+ try {
14
+ setContext(CONTEXT_KEY, node);
15
+ }
16
+ catch {
17
+ // setContext throws outside component init
18
+ }
19
+ let nodes = registry.get(file);
20
+ if (!nodes) {
21
+ nodes = new Set();
22
+ registry.set(file, nodes);
23
+ }
24
+ nodes.add(node);
25
+ try {
26
+ onDestroy(() => {
27
+ const set = registry.get(file);
28
+ if (set) {
29
+ set.delete(node);
30
+ if (set.size === 0) {
31
+ registry.delete(file);
32
+ }
33
+ }
34
+ });
35
+ }
36
+ catch {
37
+ // onDestroy throws outside component init
38
+ }
39
+ return node;
40
+ }
41
+ export function getComponentTreeContext() {
42
+ try {
43
+ return getContext(CONTEXT_KEY) ?? null;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ export function lookupComponentTree(fileName, ancestor) {
50
+ const node = findNode(fileName, ancestor);
51
+ if (!node) {
52
+ return [];
53
+ }
54
+ const hierarchy = [];
55
+ let current = node;
56
+ const seen = new Set();
57
+ while (current && !seen.has(current)) {
58
+ seen.add(current);
59
+ hierarchy.push(current.name);
60
+ current = current.parent;
61
+ }
62
+ return hierarchy;
63
+ }
64
+ export function findNode(fileName, ancestor) {
65
+ const normalizedLookup = normalizePath(fileName);
66
+ const candidates = [];
67
+ for (const [registeredFile, nodes] of registry) {
68
+ const normalizedFile = normalizePath(registeredFile);
69
+ const match = normalizedLookup === normalizedFile ||
70
+ normalizedLookup.endsWith(normalizedFile) ||
71
+ normalizedFile.endsWith(normalizedLookup);
72
+ if (match) {
73
+ for (const node of nodes) {
74
+ candidates.push(node);
75
+ }
76
+ }
77
+ }
78
+ if (candidates.length === 0) {
79
+ return undefined;
80
+ }
81
+ if (ancestor && candidates.length > 1) {
82
+ return candidates.find((c) => hasAncestor(c, ancestor)) ?? candidates[0];
83
+ }
84
+ return candidates[0];
85
+ }
86
+ function hasAncestor(node, ancestor) {
87
+ let current = node.parent;
88
+ const seen = new Set();
89
+ while (current && !seen.has(current)) {
90
+ if (current === ancestor)
91
+ return true;
92
+ seen.add(current);
93
+ current = current.parent;
94
+ }
95
+ return false;
96
+ }
97
+ function normalizePath(filePath) {
98
+ return filePath.replace(/\\/g, '/').replace(/^.*?\/src\//, 'src/');
99
+ }
@@ -0,0 +1,3 @@
1
+ import type { Attributes } from '@flareapp/js';
2
+ import type { FlareSvelteContext } from './types.js';
3
+ export declare function contextToAttributes(context: FlareSvelteContext): Attributes;
@@ -0,0 +1,7 @@
1
+ export function contextToAttributes(context) {
2
+ return {
3
+ 'context.custom': {
4
+ svelte: context.svelte,
5
+ },
6
+ };
7
+ }
@@ -0,0 +1,17 @@
1
+ import type { ComponentTreeNode } from './componentTree.js';
2
+ import type { FlareSvelteContext } from './types.js';
3
+ export interface FlareErrorHandlerOptions {
4
+ ancestor?: ComponentTreeNode | null;
5
+ beforeEvaluate?: (params: {
6
+ error: Error;
7
+ }) => void;
8
+ beforeSubmit?: (params: {
9
+ error: Error;
10
+ context: FlareSvelteContext;
11
+ }) => FlareSvelteContext;
12
+ afterSubmit?: (params: {
13
+ error: Error;
14
+ context: FlareSvelteContext;
15
+ }) => void;
16
+ }
17
+ export declare function createFlareErrorHandler(options?: FlareErrorHandlerOptions): (rawError: unknown, _reset: () => void) => Promise<void>;
@@ -0,0 +1,34 @@
1
+ import { convertToError, flare } from '@flareapp/js';
2
+ import ErrorStackParser from 'error-stack-parser';
3
+ import { contextToAttributes } from './contextToAttributes.js';
4
+ import { extractComponentInfo } from './extractComponentInfo.js';
5
+ import { getErrorOrigin } from './getErrorOrigin.js';
6
+ import { registerSvelteSdkIdentity } from './identify.js';
7
+ registerSvelteSdkIdentity();
8
+ export function createFlareErrorHandler(options) {
9
+ return async (rawError, _reset) => {
10
+ const error = convertToError(rawError);
11
+ options?.beforeEvaluate?.({ error });
12
+ let frames = [];
13
+ try {
14
+ frames = ErrorStackParser.parse(error);
15
+ }
16
+ catch {
17
+ // unparseable stack
18
+ }
19
+ const { componentName, componentHierarchy } = extractComponentInfo(frames, options?.ancestor);
20
+ const errorOrigin = getErrorOrigin(frames);
21
+ let context = {
22
+ svelte: {
23
+ componentName,
24
+ componentHierarchy,
25
+ errorOrigin,
26
+ },
27
+ };
28
+ if (options?.beforeSubmit) {
29
+ context = options.beforeSubmit({ error, context });
30
+ }
31
+ flare.reportSilently(error, contextToAttributes(context));
32
+ options?.afterSubmit?.({ error, context });
33
+ };
34
+ }
@@ -0,0 +1,8 @@
1
+ import type ErrorStackParser from 'error-stack-parser';
2
+ import { type ComponentTreeNode } from './componentTree.js';
3
+ interface ComponentInfo {
4
+ componentName: string | null;
5
+ componentHierarchy: string[];
6
+ }
7
+ export declare function extractComponentInfo(frames: ErrorStackParser.StackFrame[], ancestor?: ComponentTreeNode | null): ComponentInfo;
8
+ export {};
@@ -0,0 +1,42 @@
1
+ import { lookupComponentTree } from './componentTree.js';
2
+ export function extractComponentInfo(frames, ancestor) {
3
+ const svelteFrames = frames.filter((frame) => frame.fileName && frame.fileName.includes('.svelte'));
4
+ if (svelteFrames.length === 0) {
5
+ return { componentName: null, componentHierarchy: [] };
6
+ }
7
+ // Try the preprocessor-based component tree first.
8
+ // Walk each .svelte frame until we find one that was registered.
9
+ for (const frame of svelteFrames) {
10
+ if (!frame.fileName)
11
+ continue;
12
+ const treeHierarchy = lookupComponentTree(frame.fileName, ancestor);
13
+ if (treeHierarchy.length > 0) {
14
+ return {
15
+ componentName: treeHierarchy[0],
16
+ componentHierarchy: treeHierarchy,
17
+ };
18
+ }
19
+ }
20
+ // Fallback: extract names from stack frames only (no preprocessor).
21
+ const names = [];
22
+ for (const frame of svelteFrames) {
23
+ const name = extractName(frame);
24
+ if (name && name !== names[names.length - 1]) {
25
+ names.push(name);
26
+ }
27
+ }
28
+ return {
29
+ componentName: names[0] ?? null,
30
+ componentHierarchy: names,
31
+ };
32
+ }
33
+ function extractName(frame) {
34
+ if (frame.functionName && frame.functionName !== '<anonymous>' && !frame.functionName.includes('.')) {
35
+ return frame.functionName;
36
+ }
37
+ if (frame.fileName) {
38
+ const match = frame.fileName.match(/([^/]+)\.svelte/);
39
+ return match?.[1] ?? null;
40
+ }
41
+ return null;
42
+ }
@@ -0,0 +1,3 @@
1
+ import type ErrorStackParser from 'error-stack-parser';
2
+ import type { SvelteErrorOrigin } from './types.js';
3
+ export declare function getErrorOrigin(frames: ErrorStackParser.StackFrame[]): SvelteErrorOrigin;
@@ -0,0 +1,64 @@
1
+ // Heuristic classification of where a Svelte error originated, based on stack frame inspection.
2
+ //
3
+ // Svelte's onMount/beforeUpdate/$effect callbacks, DOM event handlers, and render functions
4
+ // all produce errors that look identical at the catch site (the error boundary sees a plain Error
5
+ // either way). The only distinguishing signal is the stack trace: different origins leave
6
+ // recognizable function names, file names, or DOM API calls in the frames.
7
+ //
8
+ // We check patterns in priority order: event > effect > render > unknown.
9
+ // Priority matters because an event handler inside a Svelte component would match both
10
+ // EVENT_PATTERNS and the .svelte filename check; we want 'event' to win.
11
+ // Inline event handlers (onclick, onsubmit, ...) and DOM event API calls.
12
+ // Browsers compile `onclick={handler}` to `.onclick = ...` or `addEventListener(...)`,
13
+ // so these patterns catch both Svelte's compiled output and manual DOM calls.
14
+ const EVENT_PATTERNS = [
15
+ /\.onclick\b/i,
16
+ /\.onsubmit\b/i,
17
+ /\.onchange\b/i,
18
+ /\.oninput\b/i,
19
+ /\.onkeydown\b/i,
20
+ /\.onkeyup\b/i,
21
+ /\.onfocus\b/i,
22
+ /\.onblur\b/i,
23
+ /\.onmouse/i,
24
+ /\.onpointer/i,
25
+ /\.ontouch/i,
26
+ /addEventListener/,
27
+ /dispatchEvent/,
28
+ /EventTarget\./,
29
+ /HTMLElement\./,
30
+ /HTMLButtonElement\./,
31
+ /HTMLInputElement\./,
32
+ /HTMLFormElement\./,
33
+ ];
34
+ // Async side-effects: $effect callbacks, onMount microtasks, promise continuations.
35
+ // Svelte 5 schedules effects via queueMicrotask; promise chains and MutationObserver
36
+ // callbacks are also async side-effects, not render-phase code.
37
+ const EFFECT_PATTERNS = [/queueMicrotask/, /Promise\.then/, /Promise\.catch/, /MutationObserver/];
38
+ // Inspects a parsed stack trace and classifies the error origin into one of:
39
+ // 'event' - error happened in a DOM event handler
40
+ // 'effect' - error happened in an async side-effect ($effect, onMount, promise)
41
+ // 'render' - error happened during Svelte component render (template/script top-level)
42
+ // 'unknown' - no frames or no recognizable pattern
43
+ export function getErrorOrigin(frames) {
44
+ if (frames.length === 0) {
45
+ return 'unknown';
46
+ }
47
+ // Flatten each frame into a single searchable string so regex matching is straightforward.
48
+ const frameStrings = frames.map((f) => `${f.functionName ?? ''} ${f.fileName ?? ''} ${f.source ?? ''}`);
49
+ // Check event patterns first (highest priority).
50
+ if (frameStrings.some((s) => EVENT_PATTERNS.some((p) => p.test(s)))) {
51
+ return 'event';
52
+ }
53
+ // Then async effect patterns.
54
+ if (frameStrings.some((s) => EFFECT_PATTERNS.some((p) => p.test(s)))) {
55
+ return 'effect';
56
+ }
57
+ // If any frame originates from a .svelte file but didn't match event/effect,
58
+ // the error most likely occurred during the synchronous render phase.
59
+ const hasSvelteFrame = frames.some((f) => f.fileName?.includes('.svelte'));
60
+ if (hasSvelteFrame) {
61
+ return 'render';
62
+ }
63
+ return 'unknown';
64
+ }
@@ -0,0 +1 @@
1
+ export declare function registerSvelteSdkIdentity(): void;
@@ -0,0 +1,10 @@
1
+ import { flare } from '@flareapp/js';
2
+ import { PACKAGE_VERSION } from './version.js';
3
+ let registered = false;
4
+ export function registerSvelteSdkIdentity() {
5
+ if (registered)
6
+ return;
7
+ registered = true;
8
+ flare.setSdkInfo({ name: '@flareapp/svelte', version: PACKAGE_VERSION });
9
+ flare.setFramework({ name: 'Svelte' });
10
+ }
@@ -0,0 +1,5 @@
1
+ export { default as FlareErrorBoundary } from './FlareErrorBoundary.svelte';
2
+ export { createFlareErrorHandler, type FlareErrorHandlerOptions } from './createFlareErrorHandler.js';
3
+ export { __flareRegisterComponent, getComponentTreeContext } from './componentTree.js';
4
+ export { flarePreprocessor, type FlarePreprocessorOptions } from './preprocessor.js';
5
+ export type { FlareSvelteContext, SvelteErrorOrigin } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { registerSvelteSdkIdentity } from './identify.js';
2
+ registerSvelteSdkIdentity();
3
+ export { default as FlareErrorBoundary } from './FlareErrorBoundary.svelte';
4
+ export { createFlareErrorHandler } from './createFlareErrorHandler.js';
5
+ export { __flareRegisterComponent, getComponentTreeContext } from './componentTree.js';
6
+ export { flarePreprocessor } from './preprocessor.js';
@@ -0,0 +1,5 @@
1
+ import type { PreprocessorGroup } from 'svelte/compiler';
2
+ export interface FlarePreprocessorOptions {
3
+ exclude?: RegExp;
4
+ }
5
+ export declare function flarePreprocessor(options?: FlarePreprocessorOptions): PreprocessorGroup;
@@ -0,0 +1,53 @@
1
+ export function flarePreprocessor(options) {
2
+ const exclude = options?.exclude;
3
+ return {
4
+ name: 'flare-component-tree',
5
+ markup({ content, filename }) {
6
+ if (!filename?.includes('.svelte')) {
7
+ return;
8
+ }
9
+ if (exclude?.test(filename)) {
10
+ return;
11
+ }
12
+ const hasScript = /<script[\s>]/i.test(content);
13
+ if (hasScript) {
14
+ return;
15
+ }
16
+ const componentName = extractComponentName(filename);
17
+ const escapedFile = escapeString(filename);
18
+ const injection = `<script>\n` +
19
+ `import { __flareRegisterComponent as __flare_reg__ } from '@flareapp/svelte';\n` +
20
+ `const __flare_node__ = __flare_reg__('${componentName}', '${escapedFile}');\n` +
21
+ `</script>\n`;
22
+ return {
23
+ code: injection + content,
24
+ };
25
+ },
26
+ script({ content, filename, attributes }) {
27
+ if (!filename?.includes('.svelte')) {
28
+ return;
29
+ }
30
+ if (exclude?.test(filename)) {
31
+ return;
32
+ }
33
+ if (attributes.context === 'module' || attributes.module != null) {
34
+ return;
35
+ }
36
+ const componentName = extractComponentName(filename);
37
+ const escapedFile = escapeString(filename);
38
+ const injection = `import { __flareRegisterComponent as __flare_reg__ } from '@flareapp/svelte';\n` +
39
+ `const __flare_node__ = __flare_reg__('${componentName}', '${escapedFile}');\n`;
40
+ return {
41
+ code: injection + content,
42
+ };
43
+ },
44
+ };
45
+ }
46
+ function extractComponentName(filename) {
47
+ const normalized = filename.replace(/\\/g, '/');
48
+ const base = normalized.split('/').pop() ?? filename;
49
+ return base.replace(/\.svelte$/, '');
50
+ }
51
+ function escapeString(str) {
52
+ return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
53
+ }
@@ -0,0 +1,8 @@
1
+ export type SvelteErrorOrigin = 'render' | 'event' | 'effect' | 'unknown';
2
+ export interface FlareSvelteContext {
3
+ svelte: {
4
+ componentName: string | null;
5
+ componentHierarchy: string[];
6
+ errorOrigin: SvelteErrorOrigin;
7
+ };
8
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const PACKAGE_VERSION = "2.0.0-alpha.0";
@@ -0,0 +1,2 @@
1
+ // generated during release, do not modify
2
+ export const PACKAGE_VERSION = '2.0.0-alpha.0';
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@flareapp/svelte",
3
+ "version": "2.0.0-alpha.0",
4
+ "description": "Svelte client for flareapp.io",
5
+ "homepage": "https://flareapp.io",
6
+ "bugs": "https://github.com/spatie/flare-client-js/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/spatie/flare-client-js.git"
10
+ },
11
+ "license": "MIT",
12
+ "author": {
13
+ "name": "Spatie",
14
+ "email": "info@spatie.be"
15
+ },
16
+ "contributors": [
17
+ "Adriaan Marain <adriaan@spatie.be>",
18
+ "Alex Vanderbist <alex@spatie.be>",
19
+ "Dries Heyninck <dries@spatie.be>",
20
+ "Freek Van der Herten <freek@spatie.be>",
21
+ "Sebastian De Deyne <sebastian@spatie.be>",
22
+ "Sébastien Henau <seba@spatie.be>"
23
+ ],
24
+ "type": "module",
25
+ "main": "./dist/index.js",
26
+ "module": "./dist/index.js",
27
+ "svelte": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "svelte": "./dist/index.js",
32
+ "import": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ }
36
+ },
37
+ "./preprocessor": {
38
+ "import": {
39
+ "types": "./dist/preprocessor.d.ts",
40
+ "default": "./dist/preprocessor.js"
41
+ }
42
+ }
43
+ },
44
+ "scripts": {
45
+ "prepublishOnly": "npm run build",
46
+ "generate:version": "node ../../scripts/generate-version.mjs .",
47
+ "build": "npm run generate:version && svelte-package -i src -o dist",
48
+ "test": "vitest run",
49
+ "typescript": "tsc --noEmit",
50
+ "release": "release-it"
51
+ },
52
+ "devDependencies": {
53
+ "@flareapp/js": "file:../js",
54
+ "@sveltejs/package": "^2.5.7",
55
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
56
+ "@testing-library/svelte": "^5.0.0",
57
+ "jsdom": "^26.1.0",
58
+ "svelte": "^5.0.0",
59
+ "typescript": "^5.7.0",
60
+ "vitest": "^4.0.0"
61
+ },
62
+ "peerDependencies": {
63
+ "@flareapp/js": "^2.0.0",
64
+ "svelte": "^5.3.0"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "dependencies": {
70
+ "error-stack-parser": "^2.1.4"
71
+ }
72
+ }