@goliapkg/sentori-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ import { type ErrorInfo, type ReactNode } from 'react';
2
+ type Props = {
3
+ children: ReactNode;
4
+ /** Rendered after an error is caught. Receives the error and a
5
+ * `reset` callback that clears the boundary so retries can run. */
6
+ fallback: (props: {
7
+ error: Error;
8
+ reset: () => void;
9
+ }) => ReactNode;
10
+ /** Optional additional logging hook. Runs after Sentori capture. */
11
+ onError?: (error: Error, info: ErrorInfo) => void;
12
+ };
13
+ /**
14
+ * Wraps `<SentoriErrorBoundaryInner>` so we can grab the capture
15
+ * function from context — class components can't use hooks directly,
16
+ * but they CAN receive props from a thin functional wrapper.
17
+ */
18
+ export declare function SentoriErrorBoundary(props: Props): import("react/jsx-runtime").JSX.Element;
19
+ export {};
20
+ //# sourceMappingURL=SentoriErrorBoundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SentoriErrorBoundary.d.ts","sourceRoot":"","sources":["../src/SentoriErrorBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAIjE,KAAK,KAAK,GAAG;IACX,QAAQ,EAAE,SAAS,CAAA;IACnB;wEACoE;IACpE,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,SAAS,CAAA;IACnE,oEAAoE;IACpE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;CAClD,CAAA;AAID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,KAAK,2CAWhD"}
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Component } from 'react';
3
+ import { useSentoriCtx } from './SentoriProvider.js';
4
+ /**
5
+ * Wraps `<SentoriErrorBoundaryInner>` so we can grab the capture
6
+ * function from context — class components can't use hooks directly,
7
+ * but they CAN receive props from a thin functional wrapper.
8
+ */
9
+ export function SentoriErrorBoundary(props) {
10
+ const { captureError } = useSentoriCtx();
11
+ return (_jsx(SentoriErrorBoundaryInner, { ...props, capture: (err, info) => {
12
+ captureError(err, { tags: { source: 'react.errorBoundary' } });
13
+ props.onError?.(err, info);
14
+ } }));
15
+ }
16
+ class SentoriErrorBoundaryInner extends Component {
17
+ state = { error: null };
18
+ static getDerivedStateFromError(error) {
19
+ return { error };
20
+ }
21
+ componentDidCatch(error, info) {
22
+ this.props.capture(error, info);
23
+ }
24
+ reset = () => this.setState({ error: null });
25
+ render() {
26
+ if (this.state.error) {
27
+ return this.props.fallback({ error: this.state.error, reset: this.reset });
28
+ }
29
+ return this.props.children;
30
+ }
31
+ }
32
+ //# sourceMappingURL=SentoriErrorBoundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SentoriErrorBoundary.js","sourceRoot":"","sources":["../src/SentoriErrorBoundary.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAkC,MAAM,OAAO,CAAA;AAEjE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAapD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAY;IAC/C,MAAM,EAAE,YAAY,EAAE,GAAG,aAAa,EAAE,CAAA;IACxC,OAAO,CACL,KAAC,yBAAyB,OACpB,KAAK,EACT,OAAO,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;YACrB,YAAY,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,qBAAqB,EAAE,EAAE,CAAC,CAAA;YAC9D,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QAC5B,CAAC,GACD,CACH,CAAA;AACH,CAAC;AAED,MAAM,yBAA0B,SAAQ,SAGvC;IACC,KAAK,GAAU,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IAE9B,MAAM,CAAC,wBAAwB,CAAC,KAAY;QAC1C,OAAO,EAAE,KAAK,EAAE,CAAA;IAClB,CAAC;IAED,iBAAiB,CAAC,KAAY,EAAE,IAAe;QAC7C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACjC,CAAC;IAED,KAAK,GAAG,GAAS,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAElD,MAAM;QACJ,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACrB,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;QAC5E,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAA;IAC5B,CAAC;CACF"}
@@ -0,0 +1,21 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { SentoriContextValue, SentoriReactConfig } from './types.js';
3
+ /**
4
+ * Initialises the JS SDK once on mount and exposes capture / breadcrumb
5
+ * helpers via context. Safe to mount multiple times in dev (StrictMode
6
+ * double-mount): the JS SDK's own idempotency guards take care of it
7
+ * but we also dedupe here via a ref.
8
+ *
9
+ * Drop this near the root of the React tree (above any
10
+ * `<SentoriErrorBoundary>`):
11
+ *
12
+ * <SentoriProvider config={{ token, release, ingestUrl, environment }}>
13
+ * <App />
14
+ * </SentoriProvider>
15
+ */
16
+ export declare function SentoriProvider({ children, config, }: {
17
+ children: ReactNode;
18
+ config: SentoriReactConfig;
19
+ }): import("react/jsx-runtime").JSX.Element;
20
+ export declare function useSentoriCtx(): SentoriContextValue;
21
+ //# sourceMappingURL=SentoriProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SentoriProvider.d.ts","sourceRoot":"","sources":["../src/SentoriProvider.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAiB,KAAK,SAAS,EAAyC,MAAM,OAAO,CAAA;AAE5F,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAQ,MAAM,YAAY,CAAA;AAM/E;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,SAAS,CAAA;IACnB,MAAM,EAAE,kBAAkB,CAAA;CAC3B,2CAuCA;AAaD,wBAAgB,aAAa,IAAI,mBAAmB,CASnD"}
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { addBreadcrumb, captureError, captureException, initSentori, setUser, } from '@goliapkg/sentori-javascript';
3
+ import { createContext, useContext, useMemo, useRef, useState } from 'react';
4
+ const Ctx = createContext(null);
5
+ let _tags = null;
6
+ /**
7
+ * Initialises the JS SDK once on mount and exposes capture / breadcrumb
8
+ * helpers via context. Safe to mount multiple times in dev (StrictMode
9
+ * double-mount): the JS SDK's own idempotency guards take care of it
10
+ * but we also dedupe here via a ref.
11
+ *
12
+ * Drop this near the root of the React tree (above any
13
+ * `<SentoriErrorBoundary>`):
14
+ *
15
+ * <SentoriProvider config={{ token, release, ingestUrl, environment }}>
16
+ * <App />
17
+ * </SentoriProvider>
18
+ */
19
+ export function SentoriProvider({ children, config, }) {
20
+ const initialisedRef = useRef(false);
21
+ const [initialised, setInitialised] = useState(false);
22
+ if (!initialisedRef.current) {
23
+ initialisedRef.current = true;
24
+ try {
25
+ initSentori(config);
26
+ setInitialised(true);
27
+ }
28
+ catch (e) {
29
+ // Misconfiguration (bad token shape, missing fields). Surface to
30
+ // console but don't crash the app — the rest of the tree should
31
+ // still render.
32
+ // eslint-disable-next-line no-console
33
+ console.error('[sentori-react] init failed', e);
34
+ }
35
+ }
36
+ const value = useMemo(() => ({
37
+ addBreadcrumb: (type, data) => {
38
+ addBreadcrumb({ data: data ?? {}, type });
39
+ },
40
+ captureError: (err, extras) => {
41
+ captureError(err, mergeExtras(extras));
42
+ },
43
+ captureException: (err, extras) => {
44
+ captureException(err, mergeExtras(extras));
45
+ },
46
+ initialised,
47
+ setTags: (tags) => {
48
+ _tags = tags;
49
+ },
50
+ setUser,
51
+ }), [initialised]);
52
+ return _jsx(Ctx.Provider, { value: value, children: children });
53
+ }
54
+ /**
55
+ * Merge provider-scoped tags into per-call extras. Per-call wins over
56
+ * provider-scoped on conflict (matches Sentry's semantics).
57
+ */
58
+ function mergeExtras(extras) {
59
+ if (!_tags)
60
+ return extras;
61
+ return { ...extras, tags: { ..._tags, ...(extras?.tags ?? {}) } };
62
+ }
63
+ export function useSentoriCtx() {
64
+ const ctx = useContext(Ctx);
65
+ if (!ctx) {
66
+ throw new Error('[sentori-react] hook used outside <SentoriProvider>. ' +
67
+ 'Wrap your app at or above the component that calls useSentori / useCaptureError.');
68
+ }
69
+ return ctx;
70
+ }
71
+ //# sourceMappingURL=SentoriProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SentoriProvider.js","sourceRoot":"","sources":["../src/SentoriProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,GACR,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,aAAa,EAAkB,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAI5F,MAAM,GAAG,GAAG,aAAa,CAA6B,IAAI,CAAC,CAAA;AAE3D,IAAI,KAAK,GAAgB,IAAI,CAAA;AAE7B;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,QAAQ,EACR,MAAM,GAIP;IACC,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IACpC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAErD,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC;QAC5B,cAAc,CAAC,OAAO,GAAG,IAAI,CAAA;QAC7B,IAAI,CAAC;YACH,WAAW,CAAC,MAAM,CAAC,CAAA;YACnB,cAAc,CAAC,IAAI,CAAC,CAAA;QACtB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,iEAAiE;YACjE,gEAAgE;YAChE,gBAAgB;YAChB,sCAAsC;YACtC,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,CAAC,CAAC,CAAA;QACjD,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAwB,OAAO,CACxC,GAAG,EAAE,CAAC,CAAC;QACL,aAAa,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YAC5B,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3C,CAAC;QACD,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YAC5B,YAAY,CAAC,GAAG,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;QACxC,CAAC;QACD,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YAChC,gBAAgB,CAAC,GAAG,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;QAC5C,CAAC;QACD,WAAW;QACX,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YAChB,KAAK,GAAG,IAAI,CAAA;QACd,CAAC;QACD,OAAO;KACR,CAAC,EACF,CAAC,WAAW,CAAC,CACd,CAAA;IAED,OAAO,KAAC,GAAG,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAgB,CAAA;AAC9D,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAClB,MAAwE;IAExE,IAAI,CAAC,KAAK;QAAE,OAAO,MAAM,CAAA;IACzB,OAAO,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,EAAE,GAAG,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAA;AACnE,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;IAC3B,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,uDAAuD;YACrD,kFAAkF,CACrF,CAAA;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup.ts"],"names":[],"mappings":""}
@@ -0,0 +1,12 @@
1
+ import { GlobalRegistrator } from '@happy-dom/global-registrator';
2
+ GlobalRegistrator.register();
3
+ // Phase 21 sub-B: tests run under happy-dom, but the JS SDK's
4
+ // transport tries to POST to ingestUrl. Stub fetch on every layer
5
+ // happy-dom exposes — global, window, and the constructor — so
6
+ // initialisation, hook installs, and capture paths all no-op.
7
+ const stubFetch = async () => new Response('{}', { status: 202 });
8
+ globalThis.fetch = stubFetch;
9
+ window.fetch = stubFetch;
10
+ window.sendBeacon = () => true;
11
+ navigator.sendBeacon = () => true;
12
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/__tests__/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AAEjE,iBAAiB,CAAC,QAAQ,EAAE,CAAA;AAE5B,8DAA8D;AAC9D,kEAAkE;AAClE,+DAA+D;AAC/D,8DAA8D;AAC9D,MAAM,SAAS,GAAG,KAAK,IAAI,EAAE,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAChE;AAAC,UAAiC,CAAC,KAAK,GAAG,SAAS,CACpD;AAAC,MAAwC,CAAC,KAAK,GAAG,SAAS,CAG3D;AAAC,MAA8C,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC,IAAI,CACvE;AAAC,SAAiD,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC,IAAI,CAAA"}
package/lib/hooks.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { CaptureExtras, SentoriContextValue } from './types.js';
2
+ /**
3
+ * Imperative access to the SDK from inside a component. Returns the
4
+ * full context — most code only needs `captureError`, `addBreadcrumb`,
5
+ * or `setUser`.
6
+ *
7
+ * const { captureError, addBreadcrumb } = useSentori()
8
+ * try {
9
+ * await api.checkout(order)
10
+ * } catch (e) {
11
+ * captureError(e as Error, { tags: { stage: 'checkout' } })
12
+ * }
13
+ */
14
+ export declare function useSentori(): SentoriContextValue;
15
+ /**
16
+ * Wraps an async function so any thrown / rejected error is captured
17
+ * and rethrown. The wrapper is `useCallback`-stable across renders if
18
+ * `extras` is stable too — pass it inside `useMemo` if you build it
19
+ * inline.
20
+ *
21
+ * const checkout = useCaptureError(
22
+ * async (order: Order) => api.checkout(order),
23
+ * { tags: { stage: 'checkout' } },
24
+ * )
25
+ * await checkout(order) // captures + throws on failure
26
+ */
27
+ export declare function useCaptureError<TArgs extends unknown[], TRet>(fn: (...args: TArgs) => Promise<TRet> | TRet, extras?: CaptureExtras): (...args: TArgs) => Promise<TRet>;
28
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAEpE;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,IAAI,mBAAmB,CAEhD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,IAAI,EAC3D,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EAC5C,MAAM,CAAC,EAAE,aAAa,GACrB,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAanC"}
package/lib/hooks.js ADDED
@@ -0,0 +1,42 @@
1
+ import { useCallback } from 'react';
2
+ import { useSentoriCtx } from './SentoriProvider.js';
3
+ /**
4
+ * Imperative access to the SDK from inside a component. Returns the
5
+ * full context — most code only needs `captureError`, `addBreadcrumb`,
6
+ * or `setUser`.
7
+ *
8
+ * const { captureError, addBreadcrumb } = useSentori()
9
+ * try {
10
+ * await api.checkout(order)
11
+ * } catch (e) {
12
+ * captureError(e as Error, { tags: { stage: 'checkout' } })
13
+ * }
14
+ */
15
+ export function useSentori() {
16
+ return useSentoriCtx();
17
+ }
18
+ /**
19
+ * Wraps an async function so any thrown / rejected error is captured
20
+ * and rethrown. The wrapper is `useCallback`-stable across renders if
21
+ * `extras` is stable too — pass it inside `useMemo` if you build it
22
+ * inline.
23
+ *
24
+ * const checkout = useCaptureError(
25
+ * async (order: Order) => api.checkout(order),
26
+ * { tags: { stage: 'checkout' } },
27
+ * )
28
+ * await checkout(order) // captures + throws on failure
29
+ */
30
+ export function useCaptureError(fn, extras) {
31
+ const { captureError } = useSentoriCtx();
32
+ return useCallback(async (...args) => {
33
+ try {
34
+ return await fn(...args);
35
+ }
36
+ catch (e) {
37
+ captureError(e instanceof Error ? e : new Error(String(e)), extras);
38
+ throw e;
39
+ }
40
+ }, [captureError, fn, extras]);
41
+ }
42
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAIpD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,aAAa,EAAE,CAAA;AACxB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAC7B,EAA4C,EAC5C,MAAsB;IAEtB,MAAM,EAAE,YAAY,EAAE,GAAG,aAAa,EAAE,CAAA;IACxC,OAAO,WAAW,CAChB,KAAK,EAAE,GAAG,IAAW,EAAE,EAAE;QACvB,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,CAAA;QAC1B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,YAAY,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;YACnE,MAAM,CAAC,CAAA;QACT,CAAC;IACH,CAAC,EACD,CAAC,YAAY,EAAE,EAAE,EAAE,MAAM,CAAC,CAC3B,CAAA;AACH,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { SentoriProvider } from './SentoriProvider.js';
2
+ export { SentoriErrorBoundary } from './SentoriErrorBoundary.js';
3
+ export { useSentori, useCaptureError } from './hooks.js';
4
+ export type { Breadcrumb, BreadcrumbType, CaptureExtras, SentoriContextValue, SentoriReactConfig, Tags, User, } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAExD,YAAY,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,IAAI,EACJ,IAAI,GACL,MAAM,YAAY,CAAA"}
package/lib/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { SentoriProvider } from './SentoriProvider.js';
2
+ export { SentoriErrorBoundary } from './SentoriErrorBoundary.js';
3
+ export { useSentori, useCaptureError } from './hooks.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA"}
package/lib/types.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { Breadcrumb, BreadcrumbType, CaptureExtras, CommonInitOptions, Tags, User } from '@goliapkg/sentori-core';
2
+ export type SentoriReactConfig = CommonInitOptions & {
3
+ /**
4
+ * Disable the JS SDK's automatic window/process error hooks. Default
5
+ * is to leave them on so non-React errors (network handlers, top-
6
+ * level promises) still capture. Set to false if you want
7
+ * SentoriErrorBoundary to be the only entry point.
8
+ */
9
+ enableGlobalHooks?: boolean;
10
+ };
11
+ export type SentoriContextValue = {
12
+ /** Append a breadcrumb to the per-process ring buffer. */
13
+ addBreadcrumb: (type: BreadcrumbType, data?: Record<string, unknown>) => void;
14
+ /**
15
+ * Capture any thrown value. Plain `Error` is the happy path; non-
16
+ * Error values get wrapped so the dashboard still sees a stack.
17
+ */
18
+ captureError: (error: Error, extras?: CaptureExtras) => void;
19
+ /** Same as captureError. Kept for parity with the JS SDK. */
20
+ captureException: (error: Error, extras?: CaptureExtras) => void;
21
+ /** Whether SentoriProvider has finished its one-shot init. */
22
+ initialised: boolean;
23
+ /** Attach a stable user identifier to subsequent events. */
24
+ setUser: (user: null | User) => void;
25
+ /** Replace the entire tag set on subsequent events. */
26
+ setTags: (tags: Tags | null) => void;
27
+ };
28
+ export type { Breadcrumb, BreadcrumbType, CaptureExtras, Tags, User };
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,IAAI,EACJ,IAAI,EACL,MAAM,wBAAwB,CAAA;AAE/B,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG;IACnD;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,0DAA0D;IAC1D,aAAa,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAC7E;;;OAGG;IACH,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5D,6DAA6D;IAC7D,gBAAgB,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;IAChE,8DAA8D;IAC9D,WAAW,EAAE,OAAO,CAAA;IACpB,4DAA4D;IAC5D,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAA;IACpC,uDAAuD;IACvD,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAA;CACrC,CAAA;AAID,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA"}
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@goliapkg/sentori-react",
3
+ "version": "0.1.0",
4
+ "description": "React adapter for Sentori — Provider, ErrorBoundary, and hooks built on @goliapkg/sentori-javascript.",
5
+ "license": "MIT",
6
+ "homepage": "https://sentori.golia.jp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/goliajp/sentori.git",
10
+ "directory": "sdk/react"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/goliajp/sentori/issues"
14
+ },
15
+ "keywords": [
16
+ "sentori",
17
+ "error-tracking",
18
+ "react",
19
+ "error-boundary"
20
+ ],
21
+ "type": "module",
22
+ "main": "./lib/index.js",
23
+ "types": "./lib/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./lib/index.d.ts",
27
+ "default": "./lib/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "lib/",
32
+ "src/",
33
+ "README.md"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json",
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "bun test",
39
+ "prepack": "bun run build"
40
+ },
41
+ "peerDependencies": {
42
+ "react": ">=18"
43
+ },
44
+ "dependencies": {
45
+ "@goliapkg/sentori-core": "0.1.0",
46
+ "@goliapkg/sentori-javascript": "0.2.0"
47
+ },
48
+ "devDependencies": {
49
+ "@happy-dom/global-registrator": "^17",
50
+ "@testing-library/react": "^16",
51
+ "@types/bun": "latest",
52
+ "@types/react": "^19",
53
+ "react": "^19",
54
+ "react-dom": "^19",
55
+ "typescript": "^5"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }
@@ -0,0 +1,56 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from 'react'
2
+
3
+ import { useSentoriCtx } from './SentoriProvider.js'
4
+
5
+ type Props = {
6
+ children: ReactNode
7
+ /** Rendered after an error is caught. Receives the error and a
8
+ * `reset` callback that clears the boundary so retries can run. */
9
+ fallback: (props: { error: Error; reset: () => void }) => ReactNode
10
+ /** Optional additional logging hook. Runs after Sentori capture. */
11
+ onError?: (error: Error, info: ErrorInfo) => void
12
+ }
13
+
14
+ type State = { error: Error | null }
15
+
16
+ /**
17
+ * Wraps `<SentoriErrorBoundaryInner>` so we can grab the capture
18
+ * function from context — class components can't use hooks directly,
19
+ * but they CAN receive props from a thin functional wrapper.
20
+ */
21
+ export function SentoriErrorBoundary(props: Props) {
22
+ const { captureError } = useSentoriCtx()
23
+ return (
24
+ <SentoriErrorBoundaryInner
25
+ {...props}
26
+ capture={(err, info) => {
27
+ captureError(err, { tags: { source: 'react.errorBoundary' } })
28
+ props.onError?.(err, info)
29
+ }}
30
+ />
31
+ )
32
+ }
33
+
34
+ class SentoriErrorBoundaryInner extends Component<
35
+ Props & { capture: (e: Error, info: ErrorInfo) => void },
36
+ State
37
+ > {
38
+ state: State = { error: null }
39
+
40
+ static getDerivedStateFromError(error: Error): State {
41
+ return { error }
42
+ }
43
+
44
+ componentDidCatch(error: Error, info: ErrorInfo): void {
45
+ this.props.capture(error, info)
46
+ }
47
+
48
+ reset = (): void => this.setState({ error: null })
49
+
50
+ render(): ReactNode {
51
+ if (this.state.error) {
52
+ return this.props.fallback({ error: this.state.error, reset: this.reset })
53
+ }
54
+ return this.props.children
55
+ }
56
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ addBreadcrumb,
3
+ captureError,
4
+ captureException,
5
+ initSentori,
6
+ setUser,
7
+ } from '@goliapkg/sentori-javascript'
8
+ import { createContext, type ReactNode, useContext, useMemo, useRef, useState } from 'react'
9
+
10
+ import type { SentoriContextValue, SentoriReactConfig, Tags } from './types.js'
11
+
12
+ const Ctx = createContext<null | SentoriContextValue>(null)
13
+
14
+ let _tags: null | Tags = null
15
+
16
+ /**
17
+ * Initialises the JS SDK once on mount and exposes capture / breadcrumb
18
+ * helpers via context. Safe to mount multiple times in dev (StrictMode
19
+ * double-mount): the JS SDK's own idempotency guards take care of it
20
+ * but we also dedupe here via a ref.
21
+ *
22
+ * Drop this near the root of the React tree (above any
23
+ * `<SentoriErrorBoundary>`):
24
+ *
25
+ * <SentoriProvider config={{ token, release, ingestUrl, environment }}>
26
+ * <App />
27
+ * </SentoriProvider>
28
+ */
29
+ export function SentoriProvider({
30
+ children,
31
+ config,
32
+ }: {
33
+ children: ReactNode
34
+ config: SentoriReactConfig
35
+ }) {
36
+ const initialisedRef = useRef(false)
37
+ const [initialised, setInitialised] = useState(false)
38
+
39
+ if (!initialisedRef.current) {
40
+ initialisedRef.current = true
41
+ try {
42
+ initSentori(config)
43
+ setInitialised(true)
44
+ } catch (e) {
45
+ // Misconfiguration (bad token shape, missing fields). Surface to
46
+ // console but don't crash the app — the rest of the tree should
47
+ // still render.
48
+ // eslint-disable-next-line no-console
49
+ console.error('[sentori-react] init failed', e)
50
+ }
51
+ }
52
+
53
+ const value: SentoriContextValue = useMemo(
54
+ () => ({
55
+ addBreadcrumb: (type, data) => {
56
+ addBreadcrumb({ data: data ?? {}, type })
57
+ },
58
+ captureError: (err, extras) => {
59
+ captureError(err, mergeExtras(extras))
60
+ },
61
+ captureException: (err, extras) => {
62
+ captureException(err, mergeExtras(extras))
63
+ },
64
+ initialised,
65
+ setTags: (tags) => {
66
+ _tags = tags
67
+ },
68
+ setUser,
69
+ }),
70
+ [initialised],
71
+ )
72
+
73
+ return <Ctx.Provider value={value}>{children}</Ctx.Provider>
74
+ }
75
+
76
+ /**
77
+ * Merge provider-scoped tags into per-call extras. Per-call wins over
78
+ * provider-scoped on conflict (matches Sentry's semantics).
79
+ */
80
+ function mergeExtras(
81
+ extras?: { fingerprint?: string[]; tags?: Tags; user?: { id?: string } },
82
+ ): typeof extras {
83
+ if (!_tags) return extras
84
+ return { ...extras, tags: { ..._tags, ...(extras?.tags ?? {}) } }
85
+ }
86
+
87
+ export function useSentoriCtx(): SentoriContextValue {
88
+ const ctx = useContext(Ctx)
89
+ if (!ctx) {
90
+ throw new Error(
91
+ '[sentori-react] hook used outside <SentoriProvider>. ' +
92
+ 'Wrap your app at or above the component that calls useSentori / useCaptureError.',
93
+ )
94
+ }
95
+ return ctx
96
+ }
@@ -0,0 +1,51 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, expect, test } from 'bun:test'
3
+
4
+ import { SentoriErrorBoundary } from '../SentoriErrorBoundary.js'
5
+ import { SentoriProvider } from '../SentoriProvider.js'
6
+
7
+ const PROVIDER_PROPS = {
8
+ config: {
9
+ environment: 'test',
10
+ ingestUrl: 'http://localhost:0',
11
+ release: 'test@0.0.0',
12
+ token: 'st_pk_testtesttesttesttesttesttest',
13
+ },
14
+ }
15
+
16
+ const Boom = (): never => {
17
+ throw new Error('boom-from-render')
18
+ }
19
+
20
+ describe('SentoriErrorBoundary', () => {
21
+ test('renders children when nothing throws', () => {
22
+ render(
23
+ <SentoriProvider {...PROVIDER_PROPS}>
24
+ <SentoriErrorBoundary fallback={() => <div>fallback</div>}>
25
+ <div>ok</div>
26
+ </SentoriErrorBoundary>
27
+ </SentoriProvider>,
28
+ )
29
+ expect(screen.getByText('ok')).toBeDefined()
30
+ })
31
+
32
+ test('renders fallback when child throws', () => {
33
+ // Suppress React's noisy "uncaught error" log in the test output.
34
+ const original = console.error
35
+ console.error = () => {}
36
+ try {
37
+ render(
38
+ <SentoriProvider {...PROVIDER_PROPS}>
39
+ <SentoriErrorBoundary
40
+ fallback={({ error }) => <div>caught: {error.message}</div>}
41
+ >
42
+ <Boom />
43
+ </SentoriErrorBoundary>
44
+ </SentoriProvider>,
45
+ )
46
+ } finally {
47
+ console.error = original
48
+ }
49
+ expect(screen.getByText('caught: boom-from-render')).toBeDefined()
50
+ })
51
+ })
@@ -0,0 +1,48 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, expect, test } from 'bun:test'
3
+
4
+ import { SentoriProvider } from '../SentoriProvider.js'
5
+ import { useSentori } from '../hooks.js'
6
+
7
+ const PROVIDER_PROPS = {
8
+ config: {
9
+ environment: 'test',
10
+ ingestUrl: 'http://localhost:0',
11
+ release: 'test@0.0.0',
12
+ token: 'st_pk_testtesttesttesttesttesttest',
13
+ },
14
+ }
15
+
16
+ describe('useSentori', () => {
17
+ test('returns capture / setUser / addBreadcrumb', () => {
18
+ const Probe = () => {
19
+ const ctx = useSentori()
20
+ return (
21
+ <div>
22
+ init={String(ctx.initialised)} hasCapture=
23
+ {String(typeof ctx.captureError === 'function')}
24
+ </div>
25
+ )
26
+ }
27
+ render(
28
+ <SentoriProvider {...PROVIDER_PROPS}>
29
+ <Probe />
30
+ </SentoriProvider>,
31
+ )
32
+ expect(screen.getByText(/init=true hasCapture=true/)).toBeDefined()
33
+ })
34
+
35
+ test('throws when used outside provider', () => {
36
+ const Probe = () => {
37
+ useSentori()
38
+ return null
39
+ }
40
+ const original = console.error
41
+ console.error = () => {}
42
+ try {
43
+ expect(() => render(<Probe />)).toThrow(/SentoriProvider/)
44
+ } finally {
45
+ console.error = original
46
+ }
47
+ })
48
+ })
@@ -0,0 +1,15 @@
1
+ import { GlobalRegistrator } from '@happy-dom/global-registrator'
2
+
3
+ GlobalRegistrator.register()
4
+
5
+ // Phase 21 sub-B: tests run under happy-dom, but the JS SDK's
6
+ // transport tries to POST to ingestUrl. Stub fetch on every layer
7
+ // happy-dom exposes — global, window, and the constructor — so
8
+ // initialisation, hook installs, and capture paths all no-op.
9
+ const stubFetch = async () => new Response('{}', { status: 202 })
10
+ ;(globalThis as { fetch: unknown }).fetch = stubFetch
11
+ ;(window as unknown as { fetch: unknown }).fetch = stubFetch
12
+ // Some happy-dom versions snapshot the original fetch constructor
13
+ // onto window.Window.prototype; overwriting our reference is harmless.
14
+ ;(window as unknown as { sendBeacon?: unknown }).sendBeacon = () => true
15
+ ;(navigator as unknown as { sendBeacon?: unknown }).sendBeacon = () => true
package/src/hooks.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { useCallback } from 'react'
2
+
3
+ import { useSentoriCtx } from './SentoriProvider.js'
4
+
5
+ import type { CaptureExtras, SentoriContextValue } from './types.js'
6
+
7
+ /**
8
+ * Imperative access to the SDK from inside a component. Returns the
9
+ * full context — most code only needs `captureError`, `addBreadcrumb`,
10
+ * or `setUser`.
11
+ *
12
+ * const { captureError, addBreadcrumb } = useSentori()
13
+ * try {
14
+ * await api.checkout(order)
15
+ * } catch (e) {
16
+ * captureError(e as Error, { tags: { stage: 'checkout' } })
17
+ * }
18
+ */
19
+ export function useSentori(): SentoriContextValue {
20
+ return useSentoriCtx()
21
+ }
22
+
23
+ /**
24
+ * Wraps an async function so any thrown / rejected error is captured
25
+ * and rethrown. The wrapper is `useCallback`-stable across renders if
26
+ * `extras` is stable too — pass it inside `useMemo` if you build it
27
+ * inline.
28
+ *
29
+ * const checkout = useCaptureError(
30
+ * async (order: Order) => api.checkout(order),
31
+ * { tags: { stage: 'checkout' } },
32
+ * )
33
+ * await checkout(order) // captures + throws on failure
34
+ */
35
+ export function useCaptureError<TArgs extends unknown[], TRet>(
36
+ fn: (...args: TArgs) => Promise<TRet> | TRet,
37
+ extras?: CaptureExtras,
38
+ ): (...args: TArgs) => Promise<TRet> {
39
+ const { captureError } = useSentoriCtx()
40
+ return useCallback(
41
+ async (...args: TArgs) => {
42
+ try {
43
+ return await fn(...args)
44
+ } catch (e) {
45
+ captureError(e instanceof Error ? e : new Error(String(e)), extras)
46
+ throw e
47
+ }
48
+ },
49
+ [captureError, fn, extras],
50
+ )
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export { SentoriProvider } from './SentoriProvider.js'
2
+ export { SentoriErrorBoundary } from './SentoriErrorBoundary.js'
3
+ export { useSentori, useCaptureError } from './hooks.js'
4
+
5
+ export type {
6
+ Breadcrumb,
7
+ BreadcrumbType,
8
+ CaptureExtras,
9
+ SentoriContextValue,
10
+ SentoriReactConfig,
11
+ Tags,
12
+ User,
13
+ } from './types.js'
package/src/types.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type {
2
+ Breadcrumb,
3
+ BreadcrumbType,
4
+ CaptureExtras,
5
+ CommonInitOptions,
6
+ Tags,
7
+ User,
8
+ } from '@goliapkg/sentori-core'
9
+
10
+ export type SentoriReactConfig = CommonInitOptions & {
11
+ /**
12
+ * Disable the JS SDK's automatic window/process error hooks. Default
13
+ * is to leave them on so non-React errors (network handlers, top-
14
+ * level promises) still capture. Set to false if you want
15
+ * SentoriErrorBoundary to be the only entry point.
16
+ */
17
+ enableGlobalHooks?: boolean
18
+ }
19
+
20
+ export type SentoriContextValue = {
21
+ /** Append a breadcrumb to the per-process ring buffer. */
22
+ addBreadcrumb: (type: BreadcrumbType, data?: Record<string, unknown>) => void
23
+ /**
24
+ * Capture any thrown value. Plain `Error` is the happy path; non-
25
+ * Error values get wrapped so the dashboard still sees a stack.
26
+ */
27
+ captureError: (error: Error, extras?: CaptureExtras) => void
28
+ /** Same as captureError. Kept for parity with the JS SDK. */
29
+ captureException: (error: Error, extras?: CaptureExtras) => void
30
+ /** Whether SentoriProvider has finished its one-shot init. */
31
+ initialised: boolean
32
+ /** Attach a stable user identifier to subsequent events. */
33
+ setUser: (user: null | User) => void
34
+ /** Replace the entire tag set on subsequent events. */
35
+ setTags: (tags: Tags | null) => void
36
+ }
37
+
38
+ // Re-exports so consumers don't have to depend on @goliapkg/sentori-core
39
+ // directly to type-annotate the props they pass in.
40
+ export type { Breadcrumb, BreadcrumbType, CaptureExtras, Tags, User }