@marktiderman/genesis-native 0.2.0 → 0.3.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 +354 -10
- package/dist/block-C5-noMhB.d.ts +223 -0
- package/dist/blocks-a1YQ1Elf.d.ts +1540 -0
- package/dist/chunk-65P7RU4V.js +49 -0
- package/dist/chunk-65P7RU4V.js.map +1 -0
- package/dist/chunk-6BT4PTGH.js +37 -0
- package/dist/chunk-6BT4PTGH.js.map +1 -0
- package/dist/chunk-75YP72G6.js +374 -0
- package/dist/chunk-75YP72G6.js.map +1 -0
- package/dist/chunk-NGYNYJPL.js +90 -0
- package/dist/chunk-NGYNYJPL.js.map +1 -0
- package/dist/chunk-VKTIHNAV.js +95 -0
- package/dist/chunk-VKTIHNAV.js.map +1 -0
- package/dist/{chunk-MMTPWUWI.js → chunk-VTJDIKFC.js} +9 -6
- package/dist/chunk-VTJDIKFC.js.map +1 -0
- package/dist/chunk-XE7LC7K6.js +29 -0
- package/dist/chunk-XE7LC7K6.js.map +1 -0
- package/dist/dev-warnings.d.ts +34 -0
- package/dist/dev-warnings.js +15 -0
- package/dist/dev-warnings.js.map +1 -0
- package/dist/devtools.d.ts +144 -0
- package/dist/devtools.js +390 -0
- package/dist/devtools.js.map +1 -0
- package/dist/index.d.ts +12 -368
- package/dist/index.js +88 -191
- package/dist/index.js.map +1 -1
- package/dist/observability.d.ts +170 -0
- package/dist/observability.js +123 -0
- package/dist/observability.js.map +1 -0
- package/dist/persist.d.ts +1 -1
- package/dist/persist.js +1 -1
- package/dist/push.d.ts +169 -0
- package/dist/push.js +996 -0
- package/dist/push.js.map +1 -0
- package/dist/{query-DhH3lLFa.d.ts → query-DUmVAfsF.d.ts} +11 -6
- package/dist/updates.d.ts +8 -1
- package/dist/updates.js +44 -69
- package/dist/updates.js.map +1 -1
- package/package.json +222 -5
- package/src/app.tsx +491 -0
- package/src/auth.tsx +176 -0
- package/src/blocks.tsx +144 -0
- package/src/boot.tsx +188 -0
- package/src/dev-warnings.ts +43 -0
- package/src/devtools/block.tsx +488 -0
- package/src/devtools/gestures.ts +107 -0
- package/src/devtools/index.ts +22 -0
- package/src/devtools/info.ts +45 -0
- package/src/devtools/slots.ts +21 -0
- package/src/errors.tsx +195 -0
- package/src/held-link.ts +90 -0
- package/src/hydration-gate.ts +88 -0
- package/src/index.ts +83 -0
- package/src/links.ts +140 -0
- package/src/nativewind.d.ts +41 -0
- package/src/observability/block.tsx +300 -0
- package/src/observability/events.ts +89 -0
- package/src/observability/index.ts +44 -0
- package/src/observability/scrub.ts +166 -0
- package/src/observability/sentry.ts +222 -0
- package/src/observability/sink.ts +147 -0
- package/src/persist.ts +83 -0
- package/src/push/backend.ts +129 -0
- package/src/push/block.tsx +545 -0
- package/src/push/index.ts +102 -0
- package/src/push/modules.ts +111 -0
- package/src/push/permission.ts +191 -0
- package/src/push/preferences.ts +136 -0
- package/src/push/quiet-hours.ts +71 -0
- package/src/push/registration.ts +189 -0
- package/src/push/routing.ts +27 -0
- package/src/push/screens.tsx +46 -0
- package/src/push/settings.tsx +378 -0
- package/src/query.ts +100 -0
- package/src/updates/block.tsx +442 -0
- package/src/updates/channels.ts +131 -0
- package/src/updates/checker.ts +190 -0
- package/src/updates/index.ts +87 -0
- package/src/updates/modules.ts +64 -0
- package/src/updates/policy.ts +314 -0
- package/src/updates/screens.tsx +159 -0
- package/src/updates/signature.ts +80 -0
- package/src/updates/store.ts +33 -0
- package/src/updates/version.ts +39 -0
- package/src/updates/whats-new.ts +122 -0
- package/dist/block-Dh5ny06W.d.ts +0 -535
- package/dist/chunk-MMTPWUWI.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ everything above your screens, so the root layout is a few lines:
|
|
|
6
6
|
```tsx
|
|
7
7
|
// app/_layout.tsx
|
|
8
8
|
import * as SplashScreen from "expo-splash-screen";
|
|
9
|
-
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
|
|
9
|
+
import { KeyboardAwareScrollView, KeyboardProvider } from "react-native-keyboard-controller";
|
|
10
10
|
import { Stack, router } from "expo-router";
|
|
11
11
|
import { GenesisNativeApp } from "@marktiderman/genesis-native";
|
|
12
12
|
import { brand, supabase, steps, persist, linkHold } from "../lib/app";
|
|
@@ -16,7 +16,7 @@ SplashScreen.preventAutoHideAsync();
|
|
|
16
16
|
export default function RootLayout() {
|
|
17
17
|
return (
|
|
18
18
|
<GenesisNativeApp brand={brand} supabase={supabase} splash={SplashScreen} steps={steps} persist={persist}
|
|
19
|
-
keyboardScroll={KeyboardAwareScrollView} links={{ hold: linkHold, onRelease: (path) => router.replace(path) }}>
|
|
19
|
+
keyboardProvider={KeyboardProvider} keyboardScroll={KeyboardAwareScrollView} links={{ hold: linkHold, onRelease: (path) => router.replace(path) }}>
|
|
20
20
|
<Stack />
|
|
21
21
|
</GenesisNativeApp>
|
|
22
22
|
);
|
|
@@ -36,8 +36,21 @@ From the outside in:
|
|
|
36
36
|
`fatal` slot itself throws, this is what shows, and "Try again" starts over.
|
|
37
37
|
2. **Theme**: `GenesisThemeProvider` with `syncNativeAppearance` on, so the
|
|
38
38
|
root owns the platform color scheme.
|
|
39
|
-
3. **Keyboard**:
|
|
40
|
-
|
|
39
|
+
3. **Keyboard**: your `keyboardProvider` (react-native-keyboard-controller's
|
|
40
|
+
`KeyboardProvider`), mounted once, then `KeyboardScrollProvider` with your
|
|
41
|
+
`keyboardScroll`, so Genesis form screens scroll the focused field into
|
|
42
|
+
view. The scroll view does nothing without the provider: give both. In
|
|
43
|
+
development, `keyboardScroll` without `keyboardProvider` logs an error.
|
|
44
|
+
`keyboardToolbar` (`KeyboardToolbar`) adds the previous / next / done bar
|
|
45
|
+
above the keyboard on Genesis's form layouts (`AuthScreen`,
|
|
46
|
+
`SettingsSubPage`); it needs `keyboardScroll`, and logs an error in
|
|
47
|
+
development without it.
|
|
48
|
+
Then `modules`, the optional native modules your app installed
|
|
49
|
+
(`GenesisModulesProvider` from `@marktiderman/genesis-ui-native`: FlashList,
|
|
50
|
+
expo-image, Reanimated, expo-glass-effect, expo-symbols, a lucide icon
|
|
51
|
+
set, the @expo/ui date picker). Each one you leave out falls back to plain
|
|
52
|
+
React Native. Read once. The update banner is Liquid Glass on iOS 26 when
|
|
53
|
+
`modules.glass` is given. See that package's README.
|
|
41
54
|
4. **Data**: `GenesisProvider` over `createSupabaseProvider(supabase)` (or
|
|
42
55
|
your `provider`) and the shell's QueryClient.
|
|
43
56
|
5. **Boot**: your `steps` (wipe a session left by a reinstall, hydrate stores,
|
|
@@ -55,16 +68,21 @@ From the outside in:
|
|
|
55
68
|
while the fatal screen is up goes to React Native's default handler, which
|
|
56
69
|
ends the app so it restarts clean. Unhandled promise rejections never reach
|
|
57
70
|
`ErrorUtils` in React Native, so on Hermes the shell installs its rejection
|
|
58
|
-
tracker (`rejectionTracker`, `null` turns it off). `onError`
|
|
59
|
-
|
|
71
|
+
tracker (`rejectionTracker`, `null` turns it off). Every block's `onError`
|
|
72
|
+
(the observability block's, which reports to Sentry) and then your
|
|
73
|
+
`onError` get all of these; a reporter that throws is ignored.
|
|
60
74
|
7. **Session, offline included**: `useSession()` is `loading`, `signedIn` or
|
|
61
75
|
`signedOut`, with `userId` and `offline`. Offline with an expired access
|
|
62
76
|
token, Supabase's `getSession()` can spend about 30 seconds on a refresh
|
|
63
77
|
and then fail; the shell waits at most `sessionWaitMs` (5 s) and on a
|
|
64
78
|
timeout or a network error carries on with the session stored on the
|
|
65
79
|
phone (`offline: true`). Supabase's own events correct it once online.
|
|
66
|
-
8. **Query**: `networkMode: "
|
|
67
|
-
|
|
80
|
+
8. **Query**: `networkMode: "offlineFirst"`: a query or mutation runs once,
|
|
81
|
+
then its retries pause while NetInfo says offline and continue when the
|
|
82
|
+
network is back, and stale queries refetch on reconnect. Offline, saved
|
|
83
|
+
data stays on screen; a query with nothing saved is `pending` with
|
|
84
|
+
`isPaused` (show "offline", not a spinner); a mutation fails at once (no
|
|
85
|
+
retries by default). With `persist`, the query-key roots you list are saved to the
|
|
68
86
|
phone for 24 hours (`maxAge`), discarded when `buster` (your app version)
|
|
69
87
|
changes, and **scoped to the signed-in user**: the user id is part of what
|
|
70
88
|
is saved, a cache saved for anyone else is deleted instead of restored, and
|
|
@@ -116,6 +134,31 @@ From the outside in:
|
|
|
116
134
|
A path the map doesn't know, or one with a `.` or `..` segment, goes to
|
|
117
135
|
`"/"` and `onUnknown`; `ignore` patterns return `null` so the router
|
|
118
136
|
leaves them alone (an auth callback).
|
|
137
|
+
11. **Sheets**: `NativeSheet` (what's new, the push prompt) is React Native's
|
|
138
|
+
own `Modal` and needs nothing above it: the system page sheet on iOS, a
|
|
139
|
+
bottom panel on Android. To use `@gorhom/bottom-sheet` instead (snap
|
|
140
|
+
points), pass it: `import * as BottomSheet from "@gorhom/bottom-sheet"`,
|
|
141
|
+
then `bottomSheet={BottomSheet}`. The shell mounts its
|
|
142
|
+
`BottomSheetModalProvider` inside the data, session and blocks providers,
|
|
143
|
+
so sheet content can read them. The library also needs gesture-handler's
|
|
144
|
+
`GestureHandlerRootView` around the app and Reanimated set up; without
|
|
145
|
+
them it throws when a sheet opens.
|
|
146
|
+
12. **Toasts**: `NativeToastProvider` is mounted around the screens, so
|
|
147
|
+
`useToast()` from genesis-ui-native works on any screen. Toasts show at
|
|
148
|
+
the top (a bottom toast would sit on a tab bar), below the safe area;
|
|
149
|
+
`toast={{ position: "bottom" }}` moves them, and `toast={null}` leaves
|
|
150
|
+
the provider to the app (one the app mounts inside the shell is the one
|
|
151
|
+
`useToast()` finds either way).
|
|
152
|
+
13. **Development warnings**: in React Native 0.88 the first `console.warn`
|
|
153
|
+
pins LogBox's "Open debugger to view warnings." pill over the bottom of
|
|
154
|
+
the screen, where a native tab bar's first tap lands on it. The warnings
|
|
155
|
+
the app can't act on (React Native's `ImageBackground` deprecation, set
|
|
156
|
+
off by NativeWind's runtime) are kept out by
|
|
157
|
+
`ignoreKnownDependencyWarnings(LogBox, __DEV__)` from
|
|
158
|
+
`@marktiderman/genesis-native/dev-warnings`, called from the app's entry
|
|
159
|
+
file before `require("expo-router/entry")` (it has to run before any
|
|
160
|
+
library loads; see the starter's `index.ts`). It removes that trigger
|
|
161
|
+
only: any other warning, the app's own included, still shows the pill.
|
|
119
162
|
|
|
120
163
|
## Slots
|
|
121
164
|
|
|
@@ -131,6 +174,7 @@ From the outside in:
|
|
|
131
174
|
| `maintenance` | `MaintenanceScreen` (with `updates`) | `{ message, retry, retrying }` |
|
|
132
175
|
| `updateBanner` | `UpdateBanner` (with `updates`) | `{ reload, dismiss, canReload, blocked, reloading }` |
|
|
133
176
|
| `whatsNew` | `WhatsNewSheet` (with `updates`) | `{ notes, version, dismiss }` |
|
|
177
|
+
| `devtoolsFeedback` | nothing (in the devtools overlay; the support block fills it) | `{ debugInfo, close }` (`close()` resolves once the sheet has slid away) |
|
|
134
178
|
|
|
135
179
|
The last four render only when the `updates` prop is given. Ejecting a
|
|
136
180
|
blocking one (`forceUpgrade: null`, `maintenance: null`) shows your screens
|
|
@@ -230,6 +274,303 @@ imported: without `updates` there is no banner and no channel switching;
|
|
|
230
274
|
without `application` nothing is forced and no notes show. The banner uses
|
|
231
275
|
`react-native-safe-area-context`, a peer (genesis-ui-native needs it too).
|
|
232
276
|
|
|
277
|
+
## Observability: errors, breadcrumbs, events, timings
|
|
278
|
+
|
|
279
|
+
`@marktiderman/genesis-native/observability` is the observability block. It is vendor-neutral:
|
|
280
|
+
everything goes through sinks, and the Sentry one is an adapter.
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
// lib/observability.ts: imported first by the module that builds GenesisNativeApp's props
|
|
284
|
+
import * as Sentry from "@sentry/react-native";
|
|
285
|
+
import * as Application from "expo-application";
|
|
286
|
+
import * as Updates from "expo-updates";
|
|
287
|
+
import { useSegments } from "expo-router";
|
|
288
|
+
import { Platform } from "react-native";
|
|
289
|
+
import { consoleSink, createObservability, defineEvents, sentrySink } from "@marktiderman/genesis-native/observability";
|
|
290
|
+
import { versionSignature } from "@marktiderman/genesis-native/updates";
|
|
291
|
+
|
|
292
|
+
export const events = defineEvents({
|
|
293
|
+
"auth.signed_in": { method: ["email", "apple"] },
|
|
294
|
+
"checkin.saved": { mood: "number", first: "boolean" },
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
export const observability = createObservability({
|
|
298
|
+
events,
|
|
299
|
+
useSegments,
|
|
300
|
+
sinks: [
|
|
301
|
+
__DEV__ && consoleSink(),
|
|
302
|
+
sentrySink(Sentry, {
|
|
303
|
+
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
|
|
304
|
+
signature: versionSignature({ application: Application, updates: Updates, platform: Platform.OS }),
|
|
305
|
+
applicationId: Application.applicationId,
|
|
306
|
+
}),
|
|
307
|
+
],
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// app/_layout.tsx
|
|
311
|
+
function RootLayout() {
|
|
312
|
+
return <GenesisNativeApp {...shell} blocks={[observability]}>…</GenesisNativeApp>;
|
|
313
|
+
}
|
|
314
|
+
export default observability.wrap(RootLayout); // Sentry.wrap: touch and profiler boundaries
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
- **One error path.** Every error the shell catches (a render error in either boundary, a global
|
|
318
|
+
error, a failed boot, an unhandled rejection) goes to the block's `onError` and then to yours.
|
|
319
|
+
A sink that throws is reported to `onProblem` and never stops the other sinks or the fatal screen.
|
|
320
|
+
`observability.captureError(error, { source: "sync" })` reports one you caught yourself.
|
|
321
|
+
- **Init order.** `sentrySink` runs `Sentry.init` when it is called, so build the block at module
|
|
322
|
+
scope, in a module the root layout imports before anything else: errors while the app's modules
|
|
323
|
+
load are caught too. A second call in the same process (Fast Refresh) reuses the client.
|
|
324
|
+
Sentry's own global handler stays installed for errors before the shell mounts; once the shell
|
|
325
|
+
is up, its handler runs first and hands the same `Error` object on, which Sentry's deduplication
|
|
326
|
+
(`__sentry_captured__`) sends once. A thrown value that isn't an `Error` can be reported twice.
|
|
327
|
+
- **PII is scrubbed on the device, by default.** Emails (percent-encoded too), phone numbers,
|
|
328
|
+
JWTs (Supabase access tokens and keys), Supabase `sb_secret_` / `sb_publishable_` keys, bearer
|
|
329
|
+
tokens, tokens in query strings, row values in PostgreSQL errors (`Failing row contains (…)`)
|
|
330
|
+
and long hex secrets are replaced in every string; values under keys naming a secret or a
|
|
331
|
+
contact detail (`authorization`, `cookie`, `apikey`, anything with `token`, `secret`, `email` or
|
|
332
|
+
`phone` in it) are replaced whole; every URL (web, deep link or path) loses its query and
|
|
333
|
+
fragment (PostgREST filters carry record ids); request bodies, cookies and PostgREST `details`
|
|
334
|
+
are dropped. Strings are cut at 4 KB first, so a huge log can't stall the JS thread. Sentry's
|
|
335
|
+
own ids (`event_id`, `trace_id`, `debug_id`, …) and processing metadata are left alone, since
|
|
336
|
+
its envelope is built from them. Sentry gets the scrubber in `beforeSend`,
|
|
337
|
+
`beforeSendTransaction`, `beforeSendSpan`, `beforeBreadcrumb`, `beforeSendLog` and
|
|
338
|
+
`beforeSendMetric` (yours run after, on the scrubbed value), plus `sendDefaultPii: false`, no
|
|
339
|
+
screenshots, view hierarchy or accessibility labels, and touch breadcrumbs without the touched
|
|
340
|
+
element's label. The user is an id only. If scrubbing throws, the event is dropped. Other sinks
|
|
341
|
+
get scrubbed errors, breadcrumbs and tags.
|
|
342
|
+
- **What it can't catch:** a person's name in free text (an error message that says "Jane
|
|
343
|
+
Doe"), and native crashes, which sentry-cocoa and sentry-android build without these
|
|
344
|
+
JavaScript hooks. Keep names out of messages; use ids.
|
|
345
|
+
- **Typed analytics.** `observability.track("auth.signed_in", { method: "apple" })` is checked
|
|
346
|
+
against `defineEvents` at compile time and at run time. A prop is a closed list of strings, a
|
|
347
|
+
number or a boolean, never free text; an unknown event, prop or value is dropped and reported.
|
|
348
|
+
- **Routes.** With `useSegments` from expo-router, each route change leaves a `navigation`
|
|
349
|
+
breadcrumb and a `route.time_on_screen` timing for the route left, by route pattern
|
|
350
|
+
(`/(app)/clients/[id]`), never the values in it. Sentry's `expoRouterIntegration` (on by
|
|
351
|
+
default in `sentrySink`) records its own navigation spans and time to initial display, so the
|
|
352
|
+
Sentry sink skips the block's navigation breadcrumbs.
|
|
353
|
+
- **Crashes count.** A fatal error (the fatal screen, a failed boot) is sent as unhandled,
|
|
354
|
+
so Sentry's release health counts it as a crash; a non-fatal one as handled.
|
|
355
|
+
- **Environments.** `development` in `__DEV__`; otherwise one per kind of build from the channel:
|
|
356
|
+
`production`, `preview` (`preview` and `pr-<number>`), `development`. Override with `environment`.
|
|
357
|
+
- **Releases.** `sentrySink` sets `release` to `<applicationId>@<version>+<build>` and `dist` to the
|
|
358
|
+
build (Sentry's own format) from the version signature, and tags every event with
|
|
359
|
+
`update.id`, `update.channel`, `update.runtime` and `update.launch`, so an over-the-air update is
|
|
360
|
+
told apart from the binary it runs on. Without an application id or version, it leaves the
|
|
361
|
+
release to Sentry rather than guess.
|
|
362
|
+
- **Without a DSN** `sentrySink` starts nothing (a warning outside development).
|
|
363
|
+
- `useRecentActivity()` gives the last breadcrumbs and errors, scrubbed (the devtools overlay
|
|
364
|
+
shows them).
|
|
365
|
+
|
|
366
|
+
**Source maps (Sentry).** Optional: add the `@sentry/react-native/expo` config plugin with your
|
|
367
|
+
organization and project, put `SENTRY_AUTH_TOKEN` in EAS secrets, use `getSentryExpoConfig` from
|
|
368
|
+
`@sentry/react-native/metro` in `metro.config.js` (Debug IDs), and for `eas update` run
|
|
369
|
+
`npx @sentry/expo-upload-sourcemaps dist` after the update. In this monorepo also set
|
|
370
|
+
`"@sentry/cli": true` under `allowBuilds` in `pnpm-workspace.yaml`, so the upload binary installs.
|
|
371
|
+
|
|
372
|
+
**EAS Observe.** EAS Observe (`expo-observe`) measures cold and warm launch, bundle load, time to
|
|
373
|
+
first render and EAS Update download time on its own once the root is wrapped
|
|
374
|
+
(`ObserveRoot.wrap`); time to interactive when you call `useObserve().markInteractive()` on your
|
|
375
|
+
entry screens; and, with `Observe.configure({ integrations: { "expo-router": true } })` at module
|
|
376
|
+
scope, per-route render and interactive times. It has no API for a custom timing metric, so
|
|
377
|
+
`observeSink(Observe)` sends `timing` as an event (`timing.<name>` with `duration_ms`, on the
|
|
378
|
+
Events page) and analytics events with `Observe.logEvent`. Its error reporting is in preview; the
|
|
379
|
+
sink leaves errors to Sentry unless `errors: true`.
|
|
380
|
+
|
|
381
|
+
`@sentry/react-native` and `expo-observe` are optional peers, passed in rather than imported, so an
|
|
382
|
+
app without them never bundles them.
|
|
383
|
+
|
|
384
|
+
## Devtools overlay
|
|
385
|
+
|
|
386
|
+
`@marktiderman/genesis-native/devtools` is a debug sheet for development, preview and internal
|
|
387
|
+
builds. It opens with a three-finger hold (every build, no native module), a shake (pass
|
|
388
|
+
`accelerometer: Accelerometer` from expo-sensors), or "Open devtools" in React Native's dev menu
|
|
389
|
+
(`devSettings: DevSettings`), and shows:
|
|
390
|
+
|
|
391
|
+
- the version signature, the update channel and a channel switch (the updates block's guarded
|
|
392
|
+
`channels`: off where that block has it off; "Back to the build's channel" always works);
|
|
393
|
+
- the signed-in user's id, the server's base URL, the blocks in effect (`useBlocks`) and your flags;
|
|
394
|
+
- the recent errors and breadcrumbs from the observability block (scrubbed);
|
|
395
|
+
- "Copy debug info" (your `copy`, else the share sheet), "Crash test" (a render error, to check the
|
|
396
|
+
fatal screen and reporting end to end), "Clear cache" (the query cache, then your `clearCache`)
|
|
397
|
+
and "Sign out" (your `signOut`);
|
|
398
|
+
- the `devtoolsFeedback` slot, which the support block fills with "Send feedback" (an annotated
|
|
399
|
+
screenshot); empty by default. `close()` (the block's, and the slot's) resolves once the sheet
|
|
400
|
+
has finished sliding away (its `onDismiss`, iOS), or after 800 ms where the platform doesn't
|
|
401
|
+
report it (Android), so a screenshot taken after it shows the app, not the sheet.
|
|
402
|
+
|
|
403
|
+
**Keep it out of production.** Import the entry only behind a condition Metro folds at build time.
|
|
404
|
+
Metro inlines `__DEV__`, babel-preset-expo inlines `process.env.EXPO_PUBLIC_*`, and in a
|
|
405
|
+
production bundle the dead branch is dropped before dependencies are collected, so the overlay's
|
|
406
|
+
code isn't in the bundle at all:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
// lib/devtools.ts: the condition must be written out here (a variable or a helper stops the folding)
|
|
410
|
+
export const devtools =
|
|
411
|
+
__DEV__ || process.env.EXPO_PUBLIC_APP_VARIANT === "development" || process.env.EXPO_PUBLIC_APP_VARIANT === "preview"
|
|
412
|
+
? require("./devtools-overlay").createAppDevtools()
|
|
413
|
+
: undefined;
|
|
414
|
+
|
|
415
|
+
// lib/devtools-overlay.ts
|
|
416
|
+
export const createAppDevtools = () =>
|
|
417
|
+
createDevtools({ enabled: __DEV__ || /\.(dev|preview)$/.test(Application.applicationId ?? ""), updates, observability, … });
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Set `EXPO_PUBLIC_APP_VARIANT` wherever `APP_VARIANT` is set (eas.json build profiles and the EAS
|
|
421
|
+
workflow jobs do, in `templates/eas`); unset, the overlay is left out. `enabled` is the second
|
|
422
|
+
lock, a fact of the binary: with it false the block mounts nothing and listens to nothing.
|
|
423
|
+
## Push: registration, taps, the badge, notification settings
|
|
424
|
+
|
|
425
|
+
`@marktiderman/genesis-native/push` is the push block, a `defineBlock`
|
|
426
|
+
declaration (`id: "push"`). Build it once and put it in `blocks`; taking it
|
|
427
|
+
out turns it off. It contributes a Settings row (`useBlockSettings`: title
|
|
428
|
+
"Notifications", section `notifications`, href `/settings/notifications`, the
|
|
429
|
+
permission as its subtitle; `settingsRow` changes or drops it) and a boot step
|
|
430
|
+
that reads the permission behind the splash (a second at most; it never fails
|
|
431
|
+
the boot), and names its migration. It runs on migration
|
|
432
|
+
0005 and the `genesis-push-send` function of
|
|
433
|
+
[`@marktiderman/genesis-supabase`](../supabase/README.md#push-the-push-block).
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
// lib/push.ts
|
|
437
|
+
import * as Notifications from "expo-notifications";
|
|
438
|
+
import * as Application from "expo-application";
|
|
439
|
+
import Constants from "expo-constants";
|
|
440
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
441
|
+
import { router } from "expo-router";
|
|
442
|
+
import { AppState, Linking, Platform } from "react-native";
|
|
443
|
+
import { createPush, supabasePushBackend } from "@marktiderman/genesis-native/push";
|
|
444
|
+
|
|
445
|
+
export const push = createPush({
|
|
446
|
+
notifications: Notifications,
|
|
447
|
+
platform: Platform.OS,
|
|
448
|
+
projectId: Constants.expoConfig?.extra?.eas?.projectId,
|
|
449
|
+
appVersion: Application.nativeApplicationVersion,
|
|
450
|
+
backend: supabasePushBackend(supabase),
|
|
451
|
+
categories: [{ id: "messages", title: "Messages" }, { id: "digest", title: "Weekly digest", defaultOn: false }],
|
|
452
|
+
storage: AsyncStorage,
|
|
453
|
+
appState: AppState,
|
|
454
|
+
openSettings: Linking.openSettings,
|
|
455
|
+
links, // the same LinkOptions as +native-intent
|
|
456
|
+
hold: linkHold, // the same hold as GenesisNativeApp's links
|
|
457
|
+
onOpen: (path) => router.push(path),
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// app/_layout.tsx
|
|
461
|
+
<GenesisNativeApp blocks={[push]} links={{ hold: linkHold, onRelease: router.replace }} … />
|
|
462
|
+
|
|
463
|
+
// app/settings/notifications.tsx
|
|
464
|
+
<NotificationSettings />
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
- **Permission, asked well.** The block never asks at launch. Call
|
|
468
|
+
`usePushPrompt().offer("when someone replies")` at the moment push is worth
|
|
469
|
+
it; the `pushPrompt` slot (default `PushPromptSheet`) says why first, and only
|
|
470
|
+
its "Turn on" brings up the system prompt. `offer` is ignored for
|
|
471
|
+
`prompt.launchGraceMs` (10 s) after start (so offer on a screen's focus, not
|
|
472
|
+
only its mount), while signed out, once the system
|
|
473
|
+
can't ask again, for `cooldownMs` (3 days) after "Not now", and for good
|
|
474
|
+
after `maxDeclines` (3). A tap on "Allow notifications" in
|
|
475
|
+
`NotificationSettings` asks straight away (the tap is the intent); once the
|
|
476
|
+
system won't ask, it opens Settings (`openSettings`). iOS provisional
|
|
477
|
+
permission counts as granted.
|
|
478
|
+
- **The token follows the session.** Once permission is granted, a signed-in
|
|
479
|
+
person's Expo push token (`getExpoPushTokenAsync({ projectId })`) is
|
|
480
|
+
registered with `genesis_push_register`, again when the OS rotates it
|
|
481
|
+
(`addPushTokenListener`) and at most hourly otherwise. Sign-out, an account
|
|
482
|
+
switch and deleting the account forget it (`genesis_push_forget`, which works
|
|
483
|
+
without a session: holding the token is the proof); a forget that fails
|
|
484
|
+
offline is kept on the phone and retried each time the app comes to the
|
|
485
|
+
front, and never undoes a later registration of the same token. Coming to the
|
|
486
|
+
front also registers again when the last try failed, and at most hourly
|
|
487
|
+
otherwise (so a token another login took comes back). Without a `projectId` there is no
|
|
488
|
+
token: the block says so once through `onError`, and Settings says so on
|
|
489
|
+
screen.
|
|
490
|
+
- **Android channels.** A `default` channel, one per category (named after it,
|
|
491
|
+
so a person can mute a category in Android's own settings) and any
|
|
492
|
+
`channels` you add, all with private lock-screen visibility, made before the
|
|
493
|
+
permission prompt and the token (Android 13 shows its prompt only once a
|
|
494
|
+
channel exists). The sender uses the category as the channel id.
|
|
495
|
+
- **While the app is open** the foreground handler shows a notification
|
|
496
|
+
(banner, list, sound, badge) unless `foreground(notification)` says no or a
|
|
497
|
+
screen hides its kind: `useSuppressNotifications((data) => data.threadId === id)`.
|
|
498
|
+
Signed out, nothing shows.
|
|
499
|
+
- **Taps.** `data.url` of a tapped notification (and of the one that launched
|
|
500
|
+
the app, `getLastNotificationResponse`) goes through `normalizeLink` with
|
|
501
|
+
`links`, once per notification: an unknown path opens `/`, another app's URL
|
|
502
|
+
never opens. Signed in, `onOpen(path)` runs after the navigator mounts;
|
|
503
|
+
signed out, the path is held in `hold` (the same `linkHold` as
|
|
504
|
+
GenesisNativeApp's `links.hold`) and followed after sign-in. Without `onOpen`
|
|
505
|
+
or `hold`, a tap opens nothing and says so through `onError`. Action buttons
|
|
506
|
+
go to `onResponse`.
|
|
507
|
+
- **Badge.** `useBadge().set(n)` / `clear()`; the sender can set it too. It is
|
|
508
|
+
cleared on sign-out (`clearBadgeOnForeground` clears it on every return to
|
|
509
|
+
the front).
|
|
510
|
+
- **NotificationSettings** is a `SettingsPage` with four parts, each a slot and
|
|
511
|
+
a hook: `permission` (`usePushPermission`), `categories`
|
|
512
|
+
(`useNotificationCategories`, a switch per category), `quietHours`
|
|
513
|
+
(`useQuietHours`: a switch, from and to times with `NativeTimePicker`, and
|
|
514
|
+
the time zone; turning it on fills in the phone's zone, and a phone that has
|
|
515
|
+
travelled offers its new one) and `privacy` (`usePrivacyPreviews`: "Show
|
|
516
|
+
previews"). `slots={{ quietHours: Mine }}` renders yours in the same section
|
|
517
|
+
with the same props; `null` drops the section. Nothing can be changed until
|
|
518
|
+
the saved settings have loaded (a change on top of the defaults would save
|
|
519
|
+
them over the person's choices); a failed load says so and is tried again
|
|
520
|
+
when the screen opens. Changes save at once and are put back, with a note,
|
|
521
|
+
when the save fails. The words are
|
|
522
|
+
`notificationSettingsCopy`, replaced with `copy`. The time pickers need
|
|
523
|
+
`@react-native-community/datetimepicker`; without it the times are shown, not
|
|
524
|
+
editable.
|
|
525
|
+
- **What the server decides.** Quiet hours and hidden previews are applied by
|
|
526
|
+
the sender (`genesis_push_targets`), in the person's saved time zone, so a
|
|
527
|
+
phone with a wrong clock can't change them, and hidden previews never leave
|
|
528
|
+
the server. The phone only shows them (`isQuietAt`, by time of day; the
|
|
529
|
+
server's window also handles clock-change nights). Android's private
|
|
530
|
+
lock-screen channel setting hides content only when the phone's own "hide
|
|
531
|
+
sensitive content" is on, and a channel's settings are the person's once it
|
|
532
|
+
exists: the server's hidden previews are the guarantee, not the channel.
|
|
533
|
+
|
|
534
|
+
Rung 3: `createPermissionStore`, `createPromptStore`, `createRegistration`,
|
|
535
|
+
`createPreferencesStore`, `supabasePushBackend` (or your own `PushBackend`),
|
|
536
|
+
`pathForResponse`, the row builders (`permissionRows`, …). Rung 4: the hooks
|
|
537
|
+
under `PushProvider`, and `PushRoot` to mount the block without
|
|
538
|
+
GenesisNativeApp (inside a `SessionProvider`).
|
|
539
|
+
|
|
540
|
+
Not covered: delivery through Apple's and Google's push services hasn't been
|
|
541
|
+
tried (it needs an EAS project with APNs and FCM credentials), and there is no
|
|
542
|
+
in-app inbox: that is `inbox-thread` in the repo's `features.ts`.
|
|
543
|
+
|
|
544
|
+
## Blocks
|
|
545
|
+
|
|
546
|
+
`blocks` takes `defineBlock` declarations (`@marktiderman/genesis-features/blocks`) a native shell
|
|
547
|
+
can run: a `NativeBlock` may also carry `bootSteps` (run behind the splash after your `steps`,
|
|
548
|
+
before the session) and a `Root` (mounted around your screens, inside the error boundary). The
|
|
549
|
+
updates block is one, so these are the same:
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
<GenesisNativeApp {...shell} updates={updates}> // as before
|
|
553
|
+
<GenesisNativeApp {...shell} blocks={[updates]}> // the same block, as a block
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
The shell resolves the blocks once, at start (`resolveBlocks` with `platform: "native"`). Then:
|
|
557
|
+
|
|
558
|
+
- their boot steps run in dependency order, and their roots wrap your screens, the first block's outermost;
|
|
559
|
+
- their `contributes.slots` fill the shell's slots by name, under yours: your `slots` (a component
|
|
560
|
+
or `null`) always win;
|
|
561
|
+
- `useBlocks()` returns the resolved blocks (ids, nav entries, routes, migrations…), each
|
|
562
|
+
contribution tagged with its FeatureId;
|
|
563
|
+
- `useBlockSettings(sections, { navigate })` returns your Settings sections with every block's
|
|
564
|
+
rows added to the section they name (an unknown section is added at the end), live subtitles
|
|
565
|
+
included. The updates block adds the version signature to `about` (`versionRow` in
|
|
566
|
+
`createUpdates` sets its title and section, or `false` turns it off). Call it at the top of the
|
|
567
|
+
Settings screen and pass the result to `SettingsPage`.
|
|
568
|
+
|
|
569
|
+
A block may require your own features that aren't blocks: list them in `appFeatures`. Blocks that
|
|
570
|
+
don't resolve (a required block that wasn't passed or is off, an unknown slot name, two blocks
|
|
571
|
+
filling one slot) hide the splash, show the fatal screen and report a `BlockError` to `onError`:
|
|
572
|
+
a set-up mistake, found at launch.
|
|
573
|
+
|
|
233
574
|
## Each part on its own
|
|
234
575
|
|
|
235
576
|
`createBoot` + `BootProvider` + `useBoot`; `GenesisErrorBoundary` +
|
|
@@ -262,7 +603,10 @@ splash (`expo-splash-screen`), storage (AsyncStorage or MMKV), `ErrorUtils`
|
|
|
262
603
|
(the global by default), the rejection tracker (Hermes's by default),
|
|
263
604
|
`AppState` and NetInfo are passed in, so it runs in tests with fakes. Peers:
|
|
264
605
|
`react`, `react-native`, `@tanstack/react-query`, for `/persist` the two
|
|
265
|
-
TanStack persister packages,
|
|
266
|
-
(with `expo-updates` and `expo-application` passed in)
|
|
606
|
+
TanStack persister packages, for `/updates` `react-native-safe-area-context`
|
|
607
|
+
(with `expo-updates` and `expo-application` passed in), and for `/push`
|
|
608
|
+
`expo-notifications`, passed in too.
|
|
609
|
+
`/observability` takes `@sentry/react-native` and `expo-observe` passed in;
|
|
610
|
+
`/devtools` takes expo-sensors' `Accelerometer` and React Native's `DevSettings` passed in.
|
|
267
611
|
|
|
268
612
|
@stability Beta
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentType, ReactNode } from 'react';
|
|
3
|
+
import { N as NativeBlock, n as ErrorReport, x as NativeBlockRootProps } from './blocks-a1YQ1Elf.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Typed analytics events: the app lists every event and the props each may
|
|
7
|
+
* carry once, and `track(name, props)` accepts nothing else. A prop is a
|
|
8
|
+
* closed list of strings, a number or a boolean, never free text, so an
|
|
9
|
+
* event can't carry a name, an email or a message someone typed.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* export const events = defineEvents({
|
|
13
|
+
* "auth.signed_in": { method: ["email", "apple"] },
|
|
14
|
+
* "settings.opened": {},
|
|
15
|
+
* "checkin.saved": { mood: "number", first: "boolean" },
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* At run time an unknown event, an unknown prop or a value outside its list
|
|
20
|
+
* is dropped and reported (a `console.warn` in development, `onInvalid`
|
|
21
|
+
* always), so a bad call is loud and still sends nothing.
|
|
22
|
+
*/
|
|
23
|
+
/** A closed list of strings, `"number"` (finite) or `"boolean"`. */
|
|
24
|
+
type EventPropSpec = readonly string[] | "number" | "boolean";
|
|
25
|
+
/** Every event, by name, with the props it may carry. */
|
|
26
|
+
type EventSchema = Readonly<Record<string, Readonly<Record<string, EventPropSpec>>>>;
|
|
27
|
+
type PropValue<S> = S extends readonly (infer L)[] ? L : S extends "number" ? number : S extends "boolean" ? boolean : never;
|
|
28
|
+
/** The props of one event: every prop optional, each its spec's type. */
|
|
29
|
+
type EventProps<S extends Readonly<Record<string, EventPropSpec>>> = {
|
|
30
|
+
readonly [K in keyof S]?: PropValue<S[K]>;
|
|
31
|
+
};
|
|
32
|
+
/** What a sink receives: the checked props. */
|
|
33
|
+
type EventValues = Readonly<Record<string, string | number | boolean>>;
|
|
34
|
+
/**
|
|
35
|
+
* Declare the app's events. Checks names (lower-case, dot-separated), prop
|
|
36
|
+
* names, and that each list has at least one short value, and throws listing
|
|
37
|
+
* every problem, so a bad schema fails where it is written.
|
|
38
|
+
*
|
|
39
|
+
* @stability Beta
|
|
40
|
+
*/
|
|
41
|
+
declare function defineEvents<const E extends EventSchema>(schema: E): E;
|
|
42
|
+
type EventCheck = {
|
|
43
|
+
ok: true;
|
|
44
|
+
props: EventValues;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
problem: string;
|
|
48
|
+
};
|
|
49
|
+
/** Check one call against the schema: the props to send, or why nothing is sent. */
|
|
50
|
+
declare function checkEvent(schema: EventSchema, name: string, props: Readonly<Record<string, unknown>> | undefined): EventCheck;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The vendor-neutral seam: a sink is where errors, breadcrumbs, the user id,
|
|
54
|
+
* analytics events and timings go. `createObservability` fans every call out
|
|
55
|
+
* to its sinks, scrubbed first; a sink that throws is reported to
|
|
56
|
+
* `onSinkError` and never takes the others (or the app) down with it.
|
|
57
|
+
*
|
|
58
|
+
* Shipped sinks: `sentrySink` (Sentry), `observeSink` (EAS Observe) and
|
|
59
|
+
* `consoleSink` (development). A vendor adapter (PostHog, Datadog…) is one
|
|
60
|
+
* object with the methods it supports.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/** Where a caught error came from and whether the app could go on. */
|
|
64
|
+
interface ErrorContext {
|
|
65
|
+
/** True when the app could not go on (a render error, a failed boot, a fatal global error). */
|
|
66
|
+
fatal: boolean;
|
|
67
|
+
/** `boundary`, `global`, `boot`, `rejection` (the shell's), or the app's own (`updates:policy`, `sync`). */
|
|
68
|
+
source: string;
|
|
69
|
+
componentStack?: string;
|
|
70
|
+
/** The route pattern on screen (`/(app)/settings`), when known. */
|
|
71
|
+
route?: string | null;
|
|
72
|
+
}
|
|
73
|
+
type BreadcrumbLevel = "debug" | "info" | "warning" | "error";
|
|
74
|
+
/** One step that led up to an error: a navigation, a tap, a request. */
|
|
75
|
+
interface Breadcrumb {
|
|
76
|
+
/** `navigation`, `ui`, `http`, `event`, or the app's own. */
|
|
77
|
+
category: string;
|
|
78
|
+
message?: string;
|
|
79
|
+
level?: BreadcrumbLevel;
|
|
80
|
+
data?: Readonly<Record<string, unknown>>;
|
|
81
|
+
/** Milliseconds since the epoch. Default now. */
|
|
82
|
+
timestamp?: number;
|
|
83
|
+
}
|
|
84
|
+
/** Low-cardinality labels on a timing (`route`, `step`), never ids or text someone typed. */
|
|
85
|
+
type TimingTags = Readonly<Record<string, string>>;
|
|
86
|
+
/**
|
|
87
|
+
* One destination. Every method is optional: a sink takes what its vendor
|
|
88
|
+
* supports. Errors arrive as an `Error` whose message is already scrubbed,
|
|
89
|
+
* unless the sink sets `scrubsErrors` (it scrubs the whole event itself and
|
|
90
|
+
* needs the original object, as Sentry does to deduplicate).
|
|
91
|
+
*
|
|
92
|
+
* @stability Beta
|
|
93
|
+
*/
|
|
94
|
+
interface ObservabilitySink {
|
|
95
|
+
readonly name: string;
|
|
96
|
+
/** The sink scrubs error events itself; give it the original `Error`. */
|
|
97
|
+
readonly scrubsErrors?: boolean;
|
|
98
|
+
captureError?(error: Error, context: ErrorContext): void;
|
|
99
|
+
breadcrumb?(crumb: Breadcrumb): void;
|
|
100
|
+
/** The signed-in login's id, or `null` when signed out. Never an email or a name. */
|
|
101
|
+
setUser?(id: string | null): void;
|
|
102
|
+
/** An analytics event, its props checked against the schema. */
|
|
103
|
+
event?(name: string, props: EventValues): void;
|
|
104
|
+
timing?(name: string, ms: number, tags?: TimingTags): void;
|
|
105
|
+
/** Wraps the root layout (Sentry's `wrap`, EAS Observe's `ObserveRoot.wrap`). */
|
|
106
|
+
wrap?<P extends Record<string, unknown>>(root: ComponentType<P>): ComponentType<P>;
|
|
107
|
+
}
|
|
108
|
+
/** A copy of `error` whose message and stack have been scrubbed (for sinks that don't scrub themselves). */
|
|
109
|
+
declare function scrubbedError(error: Error): Error;
|
|
110
|
+
interface ConsoleSinkOptions {
|
|
111
|
+
/** Default `console`. */
|
|
112
|
+
console?: Pick<Console, "log" | "warn" | "error">;
|
|
113
|
+
/** Also log breadcrumbs (noisy). Default false. */
|
|
114
|
+
breadcrumbs?: boolean;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Logs what would be sent, for development: fatal errors with `console.error`, others with `console.warn`,
|
|
118
|
+
* events and timings with `console.log`. Already scrubbed, so what you read
|
|
119
|
+
* is what a vendor sink would get.
|
|
120
|
+
*
|
|
121
|
+
* @stability Beta
|
|
122
|
+
*/
|
|
123
|
+
declare function consoleSink(options?: ConsoleSinkOptions): ObservabilitySink;
|
|
124
|
+
/**
|
|
125
|
+
* `expo-observe`'s `Observe` (SDK 56+), or a stand-in: the calls `observeSink`
|
|
126
|
+
* uses. Pass `import { Observe } from "expo-observe"`.
|
|
127
|
+
*/
|
|
128
|
+
interface ObserveLike {
|
|
129
|
+
logEvent(name: string, options?: {
|
|
130
|
+
severity?: "debug" | "info" | "warn" | "error";
|
|
131
|
+
body?: string;
|
|
132
|
+
attributes?: Record<string, unknown>;
|
|
133
|
+
}): void;
|
|
134
|
+
/** Error reporting, in preview on SDK 57+. */
|
|
135
|
+
reportError?(error: unknown): void;
|
|
136
|
+
}
|
|
137
|
+
interface ObserveSinkOptions {
|
|
138
|
+
/** Report caught errors with `Observe.reportError` (in preview). Default false: Sentry is the error tracker. */
|
|
139
|
+
errors?: boolean;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* EAS Observe. It measures launch (cold, warm, bundle load, first render),
|
|
143
|
+
* time to interactive and, with its Expo Router integration, per-route
|
|
144
|
+
* render times on its own; it has no API for a custom timing metric, so
|
|
145
|
+
* `timing` is logged as an event (`timing.<name>` with `duration_ms`) and
|
|
146
|
+
* shows on the Events page, not as a metric. Analytics events go to
|
|
147
|
+
* `Observe.logEvent` as they are.
|
|
148
|
+
*
|
|
149
|
+
* @stability Beta
|
|
150
|
+
*/
|
|
151
|
+
declare function observeSink(observe: ObserveLike, options?: ObserveSinkOptions): ObservabilitySink;
|
|
152
|
+
/** Breadcrumb data scrubbed and made plain (for sinks and the devtools overlay). */
|
|
153
|
+
declare function scrubBreadcrumb(crumb: Breadcrumb): Breadcrumb;
|
|
154
|
+
|
|
155
|
+
/** The FeatureId of the observability block. */
|
|
156
|
+
declare const OBSERVABILITY_BLOCK_ID = "observability";
|
|
157
|
+
/** A caught error as the devtools overlay shows it (scrubbed). */
|
|
158
|
+
interface RecentError {
|
|
159
|
+
at: number;
|
|
160
|
+
name: string;
|
|
161
|
+
message: string;
|
|
162
|
+
source: string;
|
|
163
|
+
fatal: boolean;
|
|
164
|
+
}
|
|
165
|
+
interface RecentActivity {
|
|
166
|
+
/** Oldest first. Navigation, events and the app's breadcrumbs. */
|
|
167
|
+
breadcrumbs: readonly Breadcrumb[];
|
|
168
|
+
/** Oldest first. */
|
|
169
|
+
errors: readonly RecentError[];
|
|
170
|
+
}
|
|
171
|
+
interface RecentStore {
|
|
172
|
+
getState(): RecentActivity;
|
|
173
|
+
subscribe(listener: () => void): () => void;
|
|
174
|
+
clear(): void;
|
|
175
|
+
}
|
|
176
|
+
interface ObservabilityOptions<E extends EventSchema = EventSchema> {
|
|
177
|
+
/** Where everything goes: `sentrySink(Sentry, …)`, `consoleSink()` in development, `observeSink(Observe)`. */
|
|
178
|
+
sinks: readonly (ObservabilitySink | null | undefined | false)[];
|
|
179
|
+
/** The app's analytics events (`defineEvents`). Without it, `track` accepts nothing. */
|
|
180
|
+
events?: E;
|
|
181
|
+
/** `useSegments` from expo-router: route breadcrumbs and `route.time_on_screen` timings. */
|
|
182
|
+
useSegments?: () => readonly string[];
|
|
183
|
+
/** How many breadcrumbs and errors `recent` keeps. Default 50 and 20. */
|
|
184
|
+
recent?: {
|
|
185
|
+
breadcrumbs?: number;
|
|
186
|
+
errors?: number;
|
|
187
|
+
};
|
|
188
|
+
/** A sink threw, or `track` was called with something the schema doesn't allow. Default `console.warn` in development. */
|
|
189
|
+
onProblem?: (problem: string, error?: unknown) => void;
|
|
190
|
+
now?: () => number;
|
|
191
|
+
}
|
|
192
|
+
interface ObservabilityBlock<E extends EventSchema = EventSchema> extends NativeBlock<typeof OBSERVABILITY_BLOCK_ID> {
|
|
193
|
+
readonly sinks: readonly ObservabilitySink[];
|
|
194
|
+
readonly events: E | undefined;
|
|
195
|
+
/** Report an error. The shell calls it for every error it catches. */
|
|
196
|
+
captureError(error: unknown, context?: Partial<ErrorContext>): void;
|
|
197
|
+
breadcrumb(crumb: Breadcrumb): void;
|
|
198
|
+
/** The login's id only. The block's root sets it from the session. */
|
|
199
|
+
setUser(id: string | null): void;
|
|
200
|
+
/** An analytics event from the schema. Anything else is dropped and reported. */
|
|
201
|
+
track<N extends keyof E & string>(name: N, props?: EventProps<E[N]>): void;
|
|
202
|
+
timing(name: string, ms: number, tags?: TimingTags): void;
|
|
203
|
+
/** The root layout wrapped by each sink that wraps (Sentry's `wrap`). Use once: `export default observability.wrap(RootLayout)`. */
|
|
204
|
+
wrap<P extends Record<string, unknown>>(root: ComponentType<P>): ComponentType<P>;
|
|
205
|
+
/** The last breadcrumbs and errors, scrubbed, for the devtools overlay. */
|
|
206
|
+
readonly recent: RecentStore;
|
|
207
|
+
/** For the shell: every error it catches. */
|
|
208
|
+
readonly onError: (error: Error, report: ErrorReport) => void;
|
|
209
|
+
readonly Root: ComponentType<NativeBlockRootProps>;
|
|
210
|
+
}
|
|
211
|
+
/** Build the block once, at module scope, before the root layout renders. @stability Beta */
|
|
212
|
+
declare function createObservability<const E extends EventSchema = EventSchema>(options: ObservabilityOptions<E>): ObservabilityBlock<E>;
|
|
213
|
+
/** Provides a block to the hooks without its root (rung 4). @stability Beta */
|
|
214
|
+
declare function ObservabilityProvider({ block, children }: {
|
|
215
|
+
block: ObservabilityBlock;
|
|
216
|
+
children?: ReactNode;
|
|
217
|
+
}): react.JSX.Element;
|
|
218
|
+
/** The block, or `null` when it is off (so a screen can call it whether or not the block is on). @stability Beta */
|
|
219
|
+
declare function useObservability(): ObservabilityBlock | null;
|
|
220
|
+
/** The last breadcrumbs and errors, scrubbed; empty when the block is off. @stability Beta */
|
|
221
|
+
declare function useRecentActivity(block?: ObservabilityBlock | null): RecentActivity;
|
|
222
|
+
|
|
223
|
+
export { type Breadcrumb as B, type ConsoleSinkOptions as C, type ErrorContext as E, type ObservabilityBlock as O, type RecentActivity as R, type TimingTags as T, type ObservabilitySink as a, type BreadcrumbLevel as b, type EventCheck as c, type EventPropSpec as d, type EventProps as e, type EventSchema as f, type EventValues as g, OBSERVABILITY_BLOCK_ID as h, type ObservabilityOptions as i, ObservabilityProvider as j, type ObserveLike as k, type ObserveSinkOptions as l, type RecentError as m, type RecentStore as n, checkEvent as o, consoleSink as p, createObservability as q, defineEvents as r, observeSink as s, scrubBreadcrumb as t, scrubbedError as u, useObservability as v, useRecentActivity as w };
|