@goliapkg/sentori-expo 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.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @goliapkg/sentori-expo
2
+
3
+ Expo adapter for Sentori — Config Plugin marker, runtime init helper
4
+ that reads `expo-application`, and an EAS post-build hook for source
5
+ map uploads. Built on `@goliapkg/sentori-react-native@>=0.2.0`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bunx expo install @goliapkg/sentori-expo @goliapkg/sentori-react-native expo-application
11
+ ```
12
+
13
+ ## Wire it up
14
+
15
+ ### 1. app.json
16
+
17
+ ```json
18
+ {
19
+ "expo": {
20
+ "plugins": ["@goliapkg/sentori-expo"]
21
+ }
22
+ }
23
+ ```
24
+
25
+ The plugin is currently a marker — `@goliapkg/sentori-react-native`
26
+ ships its own `expo-module.config.json`, podspec, and Android gradle,
27
+ so Expo Modules autolinking handles the native side. The plugin entry
28
+ gives us a stable extension point for future native config (SDK
29
+ version banner, opt-in crash-handler tuning, etc.) without changing
30
+ your `app.json`.
31
+
32
+ ### 2. App.tsx
33
+
34
+ ```tsx
35
+ import * as Application from 'expo-application'
36
+ import { initSentoriExpo } from '@goliapkg/sentori-expo'
37
+
38
+ initSentoriExpo({
39
+ application: Application,
40
+ token: process.env.EXPO_PUBLIC_SENTORI_TOKEN!,
41
+ })
42
+
43
+ export default function App() { /* ... */ }
44
+ ```
45
+
46
+ `initSentoriExpo`:
47
+ - Derives the release string `applicationId@version+build` from
48
+ `expo-application`.
49
+ - Defaults the environment to `dev`/`prod` via the RN `__DEV__` flag.
50
+ - Defaults the ingest URL to the public SaaS endpoint.
51
+
52
+ You can override any of those — see `InitOptions`.
53
+
54
+ If you're not on Expo's managed workflow, omit `application` and pass
55
+ `release` explicitly:
56
+
57
+ ```tsx
58
+ initSentoriExpo({
59
+ release: 'myapp@1.2.3+42',
60
+ token: process.env.EXPO_PUBLIC_SENTORI_TOKEN!,
61
+ })
62
+ ```
63
+
64
+ ### 3. EAS source map upload (optional, recommended)
65
+
66
+ Add to `eas.json`:
67
+
68
+ ```json
69
+ {
70
+ "build": {
71
+ "production": {
72
+ "hooks": {
73
+ "postPublish": [
74
+ {
75
+ "config": "@goliapkg/sentori-expo/eas-post-build",
76
+ "options": {
77
+ "release": "$EAS_BUILD_RELEASE"
78
+ }
79
+ }
80
+ ]
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ The hook shells out to `@goliapkg/sentori-cli upload sourcemap` — install
88
+ it as a dev dep:
89
+
90
+ ```bash
91
+ bun add -D @goliapkg/sentori-cli
92
+ ```
93
+
94
+ > Status: Phase 22 sub-A lands the actual `upload sourcemap` and
95
+ > `upload dsym` CLI subcommands. Until then the hook logs a warning
96
+ > and exits 0 — adopt the wiring now and the upload works
97
+ > transparently when you next bump `@goliapkg/sentori-cli`.
98
+
99
+ ## What `initSentoriExpo` does under the hood
100
+
101
+ ```
102
+ @goliapkg/sentori-expo
103
+ └── reads expo-application metadata
104
+ └── calls @goliapkg/sentori-react-native init({ token, release, ... })
105
+ └── starts the JS-layer global error / promise / network hooks
106
+ └── primes the native iOS / Android crash handlers
107
+ ```
108
+
109
+ The full feature list (breadcrumbs, capture, network instrumentation,
110
+ native crash capture) lives in `@goliapkg/sentori-react-native` —
111
+ `-expo` only adds the auto-config + EAS plumbing.
package/app.plugin.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Sentori Expo Config Plugin.
3
+ *
4
+ * `@goliapkg/sentori-react-native` already exposes
5
+ * `expo-module.config.json` + iOS podspec + Android build.gradle, so
6
+ * Expo Modules autolinking handles the native side without any
7
+ * additional config-plugins work. The Config Plugin entry exists
8
+ * mainly as a marker so users can drop:
9
+ *
10
+ * {
11
+ * "expo": {
12
+ * "plugins": ["@goliapkg/sentori-expo"]
13
+ * }
14
+ * }
15
+ *
16
+ * into their app.json without breaking the build, and so we can
17
+ * extend it later (e.g. SDK-version metadata in Info.plist /
18
+ * AndroidManifest, native crash-handler opt-ins) without changing
19
+ * the user-facing wiring.
20
+ *
21
+ * The plugin is intentionally CommonJS — Expo's plugin loader uses
22
+ * `require()`.
23
+ */
24
+ const { withInfoPlist } = require('@expo/config-plugins')
25
+
26
+ const SENTORI_VERSION_KEY = 'SentoriSdkVersion'
27
+
28
+ /**
29
+ * @param {import('@expo/config-plugins').ExpoConfig} config
30
+ * @param {{ sdkVersion?: string }} [props]
31
+ */
32
+ const withSentori = (config, props = {}) => {
33
+ return withInfoPlist(config, (cfg) => {
34
+ cfg.modResults[SENTORI_VERSION_KEY] = props.sdkVersion || '0.1.0'
35
+ return cfg
36
+ })
37
+ }
38
+
39
+ module.exports = withSentori
package/lib/index.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { ExpoApplicationLike, InitOptions } from './types.js';
2
+ /**
3
+ * Drop-in init for Expo apps. Reads bundleId / version / build from
4
+ * `expo-application` (which is shipped in every Expo SDK) so the
5
+ * caller only has to supply the token. Falls back to manual config
6
+ * fields when expo-application isn't installed (bare RN apps), in
7
+ * which case the caller MUST pass `release`.
8
+ *
9
+ * // App.tsx
10
+ * import { initSentoriExpo } from '@goliapkg/sentori-expo'
11
+ * import * as Application from 'expo-application'
12
+ *
13
+ * initSentoriExpo({
14
+ * application: Application,
15
+ * token: process.env.EXPO_PUBLIC_SENTORI_TOKEN!,
16
+ * })
17
+ *
18
+ * Why we ask the caller to import `expo-application` and pass it in,
19
+ * instead of `import * as Application from 'expo-application'` here?
20
+ * Bundlers (Metro / Hermes) statically include every import; if this
21
+ * package imported expo-application directly, every consumer would
22
+ * be forced to install it even when running in a bare-RN context.
23
+ */
24
+ export declare function initSentoriExpo(options: InitOptions): void;
25
+ /**
26
+ * Build a `slug@version+build` release string from expo-application.
27
+ * Returns `undefined` when the module isn't available so the caller
28
+ * can fall back to a manually-supplied release.
29
+ *
30
+ * Exported for callers who want to use the same string outside of
31
+ * init (e.g. as a tag, log prefix, or metric label).
32
+ */
33
+ export declare function deriveRelease(app: ExpoApplicationLike | undefined): string | undefined;
34
+ export type { ExpoApplicationLike, InitOptions } from './types.js';
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAElE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAe1D;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,mBAAmB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOtF;AAWD,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
package/lib/index.js ADDED
@@ -0,0 +1,64 @@
1
+ // Phase 21 sub-D: Expo runtime helpers.
2
+ //
3
+ // The Config Plugin (app.plugin.js) is loaded by Expo at prebuild
4
+ // time; this module is what apps import from JS at runtime.
5
+ import { init as initSentoriRN } from '@goliapkg/sentori-react-native';
6
+ /**
7
+ * Drop-in init for Expo apps. Reads bundleId / version / build from
8
+ * `expo-application` (which is shipped in every Expo SDK) so the
9
+ * caller only has to supply the token. Falls back to manual config
10
+ * fields when expo-application isn't installed (bare RN apps), in
11
+ * which case the caller MUST pass `release`.
12
+ *
13
+ * // App.tsx
14
+ * import { initSentoriExpo } from '@goliapkg/sentori-expo'
15
+ * import * as Application from 'expo-application'
16
+ *
17
+ * initSentoriExpo({
18
+ * application: Application,
19
+ * token: process.env.EXPO_PUBLIC_SENTORI_TOKEN!,
20
+ * })
21
+ *
22
+ * Why we ask the caller to import `expo-application` and pass it in,
23
+ * instead of `import * as Application from 'expo-application'` here?
24
+ * Bundlers (Metro / Hermes) statically include every import; if this
25
+ * package imported expo-application directly, every consumer would
26
+ * be forced to install it even when running in a bare-RN context.
27
+ */
28
+ export function initSentoriExpo(options) {
29
+ const release = options.release ?? deriveRelease(options.application);
30
+ if (!release) {
31
+ throw new Error('[sentori-expo] could not derive release. ' +
32
+ 'Either pass `release` explicitly, or pass `application: Application` ' +
33
+ 'from `import * as Application from "expo-application"`.');
34
+ }
35
+ initSentoriRN({
36
+ environment: options.environment ?? (isDev() ? 'dev' : 'prod'),
37
+ ingestUrl: options.ingestUrl ?? 'https://ingest.sentori.golia.jp',
38
+ release,
39
+ token: options.token,
40
+ });
41
+ }
42
+ /**
43
+ * Build a `slug@version+build` release string from expo-application.
44
+ * Returns `undefined` when the module isn't available so the caller
45
+ * can fall back to a manually-supplied release.
46
+ *
47
+ * Exported for callers who want to use the same string outside of
48
+ * init (e.g. as a tag, log prefix, or metric label).
49
+ */
50
+ export function deriveRelease(app) {
51
+ if (!app)
52
+ return undefined;
53
+ const id = app.applicationId ?? app.nativeApplicationVersion ?? 'app';
54
+ const version = app.nativeApplicationVersion ?? '0.0.0';
55
+ const build = app.nativeBuildVersion ?? '0';
56
+ return `${id}@${version}+${build}`;
57
+ }
58
+ function isDev() {
59
+ // RN's __DEV__ is true under Metro dev server; bare false in
60
+ // Hermes release builds. typeof check keeps this safe to import in
61
+ // Node tests where __DEV__ doesn't exist.
62
+ return typeof __DEV__ !== 'undefined' && __DEV__;
63
+ }
64
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,kEAAkE;AAClE,4DAA4D;AAE5D,OAAO,EAAE,IAAI,IAAI,aAAa,EAAE,MAAM,gCAAgC,CAAA;AAItE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,eAAe,CAAC,OAAoB;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,aAAa,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IACrE,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,2CAA2C;YACzC,uEAAuE;YACvE,yDAAyD,CAC5D,CAAA;IACH,CAAC;IACD,aAAa,CAAC;QACZ,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QAC9D,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,iCAAiC;QACjE,OAAO;QACP,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,GAAoC;IAChE,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAA;IAC1B,MAAM,EAAE,GACN,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,wBAAwB,IAAI,KAAK,CAAA;IAC5D,MAAM,OAAO,GAAG,GAAG,CAAC,wBAAwB,IAAI,OAAO,CAAA;IACvD,MAAM,KAAK,GAAG,GAAG,CAAC,kBAAkB,IAAI,GAAG,CAAA;IAC3C,OAAO,GAAG,EAAE,IAAI,OAAO,IAAI,KAAK,EAAE,CAAA;AACpC,CAAC;AAED,SAAS,KAAK;IACZ,6DAA6D;IAC7D,mEAAmE;IACnE,0CAA0C;IAC1C,OAAO,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAA;AAClD,CAAC"}
package/lib/types.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Subset of the `expo-application` module we read for release
3
+ * derivation. Defining a structural type instead of importing the
4
+ * module keeps `expo-application` a peer dep that the Expo runtime
5
+ * provides — bare-RN consumers don't need to install it.
6
+ */
7
+ export type ExpoApplicationLike = {
8
+ /** e.g. "com.example.myapp" — Android applicationId / iOS bundleId. */
9
+ applicationId?: null | string;
10
+ /** e.g. "5" — Android versionCode / iOS CFBundleVersion. */
11
+ nativeBuildVersion?: null | string;
12
+ /** e.g. "1.2.3" — iOS CFBundleShortVersionString / Android versionName. */
13
+ nativeApplicationVersion?: null | string;
14
+ };
15
+ export type InitOptions = {
16
+ /** Pass `import * as Application from 'expo-application'` here. */
17
+ application?: ExpoApplicationLike;
18
+ /** Override the auto-derived environment. Defaults to dev/prod via __DEV__. */
19
+ environment?: string;
20
+ /** Override ingest URL (self-hosted). Defaults to public SaaS. */
21
+ ingestUrl?: string;
22
+ /** Manual release override — required when `application` is omitted. */
23
+ release?: string;
24
+ /** Project public token, format `st_pk_...`. */
25
+ token: string;
26
+ };
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,uEAAuE;IACvE,aAAa,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;IAC7B,4DAA4D;IAC5D,kBAAkB,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;IAClC,2EAA2E;IAC3E,wBAAwB,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;CACzC,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,mEAAmE;IACnE,WAAW,CAAC,EAAE,mBAAmB,CAAA;IACjC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAA;CACd,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,67 @@
1
+ {
2
+ "name": "@goliapkg/sentori-expo",
3
+ "version": "0.1.0",
4
+ "description": "Expo adapter for Sentori — Config Plugin marker, expo-application auto-config, EAS post-build helper. Built on @goliapkg/sentori-react-native.",
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/expo"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/goliajp/sentori/issues"
14
+ },
15
+ "keywords": [
16
+ "sentori",
17
+ "error-tracking",
18
+ "expo",
19
+ "react-native",
20
+ "config-plugin"
21
+ ],
22
+ "type": "module",
23
+ "main": "./lib/index.js",
24
+ "types": "./lib/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./lib/index.d.ts",
28
+ "default": "./lib/index.js"
29
+ },
30
+ "./app.plugin.js": "./app.plugin.js",
31
+ "./eas-post-build": "./scripts/eas-post-build.mjs"
32
+ },
33
+ "files": [
34
+ "lib/",
35
+ "src/",
36
+ "app.plugin.js",
37
+ "scripts/",
38
+ "README.md"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "bun test",
44
+ "prepack": "bun run build"
45
+ },
46
+ "peerDependencies": {
47
+ "@goliapkg/sentori-react-native": ">=0.2.0",
48
+ "expo": ">=50",
49
+ "expo-application": ">=5",
50
+ "react-native": ">=0.74"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "expo-application": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "dependencies": {
58
+ "@expo/config-plugins": "^9 || ^10"
59
+ },
60
+ "devDependencies": {
61
+ "@types/bun": "latest",
62
+ "typescript": "^5"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ }
67
+ }
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * EAS post-build hook for Sentori source map upload.
4
+ *
5
+ * Wire it from app.json / eas.json:
6
+ *
7
+ * {
8
+ * "build": {
9
+ * "production": {
10
+ * "ios": { "buildArtifactPaths": ["ios/build/**\/*.dSYM"] },
11
+ * "hooks": {
12
+ * "postPublish": [
13
+ * {
14
+ * "config": "@goliapkg/sentori-expo/eas-post-build",
15
+ * "options": { "release": "myapp@1.2.3+42" }
16
+ * }
17
+ * ]
18
+ * }
19
+ * }
20
+ * }
21
+ * }
22
+ *
23
+ * Or call this script directly from a custom build hook:
24
+ *
25
+ * #!/bin/sh
26
+ * node ./node_modules/@goliapkg/sentori-expo/scripts/eas-post-build.mjs \
27
+ * --token $SENTORI_ADMIN_TOKEN --release "$EAS_BUILD_RELEASE"
28
+ *
29
+ * The script shells out to `sentori-cli` for the actual upload (Phase 22
30
+ * sub-A introduces `sentori-cli upload dsym`). Until that lands this is
31
+ * a stub that logs what it would have done — adopt sub-D in your
32
+ * pipeline now and the CLI integration arrives transparently.
33
+ */
34
+
35
+ import { spawnSync } from 'node:child_process'
36
+ import { existsSync } from 'node:fs'
37
+
38
+ const args = parseArgs(process.argv.slice(2))
39
+
40
+ const token = args.token ?? process.env.SENTORI_ADMIN_TOKEN
41
+ const release = args.release ?? process.env.EAS_BUILD_RELEASE
42
+ const ingestUrl = args.ingest ?? process.env.SENTORI_INGEST_URL
43
+
44
+ if (!token || !release) {
45
+ console.error(
46
+ '[sentori-expo:eas-post-build] missing --token or --release ' +
47
+ '(env: SENTORI_ADMIN_TOKEN, EAS_BUILD_RELEASE)',
48
+ )
49
+ process.exit(1)
50
+ }
51
+
52
+ const cli = resolveCli()
53
+ if (!cli) {
54
+ console.warn(
55
+ '[sentori-expo:eas-post-build] sentori-cli not found on PATH or in node_modules. ' +
56
+ 'Skipping upload — install @goliapkg/sentori-cli to enable. ' +
57
+ 'Phase 22 sub-A will land the proper upload subcommand.',
58
+ )
59
+ process.exit(0)
60
+ }
61
+
62
+ const cmd = [
63
+ 'upload',
64
+ 'sourcemap',
65
+ '--token',
66
+ token,
67
+ '--release',
68
+ release,
69
+ ...(ingestUrl ? ['--ingest', ingestUrl] : []),
70
+ // Default Expo build output for the JS bundle + sourcemap.
71
+ './dist',
72
+ ]
73
+
74
+ console.log(`[sentori-expo:eas-post-build] running: ${cli} ${cmd.join(' ')}`)
75
+ const r = spawnSync(cli, cmd, { stdio: 'inherit' })
76
+ process.exit(r.status ?? 0)
77
+
78
+ function parseArgs(argv) {
79
+ const out = {}
80
+ for (let i = 0; i < argv.length; i++) {
81
+ const a = argv[i]
82
+ if (a.startsWith('--')) {
83
+ out[a.slice(2)] = argv[i + 1]
84
+ i++
85
+ }
86
+ }
87
+ return out
88
+ }
89
+
90
+ function resolveCli() {
91
+ // Prefer node_modules/.bin so the locked @goliapkg/sentori-cli wins
92
+ // over a globally-installed older copy.
93
+ for (const p of [
94
+ './node_modules/.bin/sentori-cli',
95
+ './node_modules/@goliapkg/sentori-cli/bin/sentori-cli.js',
96
+ ]) {
97
+ if (existsSync(p)) return p
98
+ }
99
+ // PATH lookup as last resort.
100
+ const which = spawnSync('which', ['sentori-cli'])
101
+ const found = which.stdout?.toString().trim()
102
+ return found || null
103
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import { deriveRelease } from '../index.js'
4
+
5
+ describe('deriveRelease', () => {
6
+ test('builds slug@version+build from expo-application fields', () => {
7
+ expect(
8
+ deriveRelease({
9
+ applicationId: 'com.example.myapp',
10
+ nativeApplicationVersion: '1.2.3',
11
+ nativeBuildVersion: '42',
12
+ }),
13
+ ).toBe('com.example.myapp@1.2.3+42')
14
+ })
15
+
16
+ test('substitutes defaults for missing fields', () => {
17
+ expect(
18
+ deriveRelease({
19
+ applicationId: 'com.example.myapp',
20
+ }),
21
+ ).toBe('com.example.myapp@0.0.0+0')
22
+ })
23
+
24
+ test('returns undefined when module is missing', () => {
25
+ expect(deriveRelease(undefined)).toBeUndefined()
26
+ })
27
+
28
+ test('handles null fields gracefully (Expo bare workflow)', () => {
29
+ expect(
30
+ deriveRelease({
31
+ applicationId: null,
32
+ nativeApplicationVersion: null,
33
+ nativeBuildVersion: null,
34
+ }),
35
+ ).toBe('app@0.0.0+0')
36
+ })
37
+ })
package/src/index.ts ADDED
@@ -0,0 +1,75 @@
1
+ // Phase 21 sub-D: Expo runtime helpers.
2
+ //
3
+ // The Config Plugin (app.plugin.js) is loaded by Expo at prebuild
4
+ // time; this module is what apps import from JS at runtime.
5
+
6
+ import { init as initSentoriRN } from '@goliapkg/sentori-react-native'
7
+
8
+ import type { ExpoApplicationLike, InitOptions } from './types.js'
9
+
10
+ /**
11
+ * Drop-in init for Expo apps. Reads bundleId / version / build from
12
+ * `expo-application` (which is shipped in every Expo SDK) so the
13
+ * caller only has to supply the token. Falls back to manual config
14
+ * fields when expo-application isn't installed (bare RN apps), in
15
+ * which case the caller MUST pass `release`.
16
+ *
17
+ * // App.tsx
18
+ * import { initSentoriExpo } from '@goliapkg/sentori-expo'
19
+ * import * as Application from 'expo-application'
20
+ *
21
+ * initSentoriExpo({
22
+ * application: Application,
23
+ * token: process.env.EXPO_PUBLIC_SENTORI_TOKEN!,
24
+ * })
25
+ *
26
+ * Why we ask the caller to import `expo-application` and pass it in,
27
+ * instead of `import * as Application from 'expo-application'` here?
28
+ * Bundlers (Metro / Hermes) statically include every import; if this
29
+ * package imported expo-application directly, every consumer would
30
+ * be forced to install it even when running in a bare-RN context.
31
+ */
32
+ export function initSentoriExpo(options: InitOptions): void {
33
+ const release = options.release ?? deriveRelease(options.application)
34
+ if (!release) {
35
+ throw new Error(
36
+ '[sentori-expo] could not derive release. ' +
37
+ 'Either pass `release` explicitly, or pass `application: Application` ' +
38
+ 'from `import * as Application from "expo-application"`.',
39
+ )
40
+ }
41
+ initSentoriRN({
42
+ environment: options.environment ?? (isDev() ? 'dev' : 'prod'),
43
+ ingestUrl: options.ingestUrl ?? 'https://ingest.sentori.golia.jp',
44
+ release,
45
+ token: options.token,
46
+ })
47
+ }
48
+
49
+ /**
50
+ * Build a `slug@version+build` release string from expo-application.
51
+ * Returns `undefined` when the module isn't available so the caller
52
+ * can fall back to a manually-supplied release.
53
+ *
54
+ * Exported for callers who want to use the same string outside of
55
+ * init (e.g. as a tag, log prefix, or metric label).
56
+ */
57
+ export function deriveRelease(app: ExpoApplicationLike | undefined): string | undefined {
58
+ if (!app) return undefined
59
+ const id =
60
+ app.applicationId ?? app.nativeApplicationVersion ?? 'app'
61
+ const version = app.nativeApplicationVersion ?? '0.0.0'
62
+ const build = app.nativeBuildVersion ?? '0'
63
+ return `${id}@${version}+${build}`
64
+ }
65
+
66
+ function isDev(): boolean {
67
+ // RN's __DEV__ is true under Metro dev server; bare false in
68
+ // Hermes release builds. typeof check keeps this safe to import in
69
+ // Node tests where __DEV__ doesn't exist.
70
+ return typeof __DEV__ !== 'undefined' && __DEV__
71
+ }
72
+
73
+ declare const __DEV__: boolean
74
+
75
+ export type { ExpoApplicationLike, InitOptions } from './types.js'
package/src/types.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Subset of the `expo-application` module we read for release
3
+ * derivation. Defining a structural type instead of importing the
4
+ * module keeps `expo-application` a peer dep that the Expo runtime
5
+ * provides — bare-RN consumers don't need to install it.
6
+ */
7
+ export type ExpoApplicationLike = {
8
+ /** e.g. "com.example.myapp" — Android applicationId / iOS bundleId. */
9
+ applicationId?: null | string
10
+ /** e.g. "5" — Android versionCode / iOS CFBundleVersion. */
11
+ nativeBuildVersion?: null | string
12
+ /** e.g. "1.2.3" — iOS CFBundleShortVersionString / Android versionName. */
13
+ nativeApplicationVersion?: null | string
14
+ }
15
+
16
+ export type InitOptions = {
17
+ /** Pass `import * as Application from 'expo-application'` here. */
18
+ application?: ExpoApplicationLike
19
+ /** Override the auto-derived environment. Defaults to dev/prod via __DEV__. */
20
+ environment?: string
21
+ /** Override ingest URL (self-hosted). Defaults to public SaaS. */
22
+ ingestUrl?: string
23
+ /** Manual release override — required when `application` is omitted. */
24
+ release?: string
25
+ /** Project public token, format `st_pk_...`. */
26
+ token: string
27
+ }