Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,4 @@ packages/core/src/info.ts
dist
buildinfo.json
CMakeUserPresets.json
.npmrc
157 changes: 155 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,165 @@ This plugin contains support for Amazon Vega as a platform for React Native apps

Install the dependencies for this package:

`npm install react-native-uuid @amazon-devices/react-native-device-info @segment/analytics-react-native@beta`
```sh
npm install react-native-uuid \
@amazon-devices/react-native-device-info \
@amazon-devices/react-native-localize \
@segment/analytics-react-native
```

For React Native 0.83 (Vega SDK 0.24), also add the async-storage alias:

```sh
npm install @react-native-async-storage/async-storage@npm:@amazon-devices/react-native-async-storage__async-storage
```

Then install this package from npm registry
Then install this package from npm registry:

`npm install @segment/analytics-react-native-plugin-kepler`

### Metro configuration (Kepler 4 / Vega SDK 0.24)

Kepler 4 runs React Native's New Architecture exclusively — there is no legacy bridge (`NativeModules` / `__fbBatchedBridgeConfig`). Several dependencies of `@segment/analytics-react-native` access native modules at module-load time and crash immediately on Kepler 4. The fix is a set of Metro resolver intercepts and pure-JS shim files that replace those modules before they are loaded.

#### 1. Create the shim files

Create the following three files in your app's `src/` directory.

**`src/get-random-values-polyfill.js`**

`react-native-get-random-values` patches `crypto.getRandomValues` using a native TurboModule (`RNGetRandomValues`) that is not registered on Kepler 4. This shim installs a pure-JS fallback instead.

```js
'use strict';

if (typeof globalThis.crypto === 'undefined') {
globalThis.crypto = {};
}
if (typeof globalThis.crypto.getRandomValues !== 'function') {
globalThis.crypto.getRandomValues = function (array) {
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256);
}
return array;
};
}
```

**`src/sovran-polyfill.js`**

`@segment/sovran-react-native` accesses `NativeModules` at load time, which triggers Kepler's legacy `BatchedBridge/NativeModules.js` and throws before any try/catch can execute. This shim re-exports only the pure-JS store and persistor from sovran's pre-built output, bypassing the native bridge entirely.

```js
'use strict';

const { createStore } = require('@segment/sovran-react-native/lib/commonjs/store');
const { registerBridgeStore } = require('@segment/sovran-react-native/lib/commonjs/bridge');
const persistor = require('@segment/sovran-react-native/lib/commonjs/persistor');

module.exports = { createStore, registerBridgeStore, ...persistor };
```

**`src/context-polyfill.js`**

`analytics-react-native`'s `context.ts` calls `getNativeModule('AnalyticsReactNative')` which also triggers the legacy `NativeModules` crash. The Kepler plugin already provides its own `deviceInfoProvider` that overwrites device/OS context, so returning empty defaults here is safe.

```js
'use strict';

const { libraryInfo } = require('@segment/analytics-react-native/lib/commonjs/info');
const { getUUID } = require('@segment/analytics-react-native/lib/commonjs/uuid');

const getContext = async (userTraits = {}) => {
return {
app: { build: '', name: '', namespace: '', version: '' },
device: { id: '', manufacturer: '', model: '', name: '', type: '' },
library: { name: 'analytics-react-native', version: libraryInfo.version },
locale: '',
network: { cellular: false, wifi: false },
os: { name: '', version: '' },
screen: { width: 0, height: 0, density: 0 },
timezone: '',
traits: userTraits,
instanceId: getUUID(),
};
};

module.exports = { getContext };
```

#### 2. Configure `metro.config.js`

Wire all three shims via Metro's `resolveRequest`. **Important:** Kepler's CLI platform injects its own `resolveRequest` into `getDefaultConfig` to remap `react-native` to the Kepler system bundle. You must capture and chain it — failing to do so bundles standard React Native instead of the Kepler runtime.

```js
const path = require('path');
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

const defaultConfig = getDefaultConfig(__dirname);

// Capture Kepler's resolveRequest before merging — must be chained.
const keplerResolveRequest = defaultConfig.resolver?.resolveRequest;

const config = {
resolver: {
platforms: [...(defaultConfig.resolver?.platforms ?? []), 'kepler'],
extraNodeModules: {
// @amazon-devices/react-native-kepler references @amzn/* internally.
'@amzn/react-native-kepler': path.resolve(
__dirname,
'node_modules/@amazon-devices/react-native-kepler',
),
},
resolveRequest: (context, moduleName, platform) => {
// Intercept react-native-get-random-values — replaces broken TurboModule
// with a pure-JS Math.random implementation.
if (moduleName === 'react-native-get-random-values') {
return {
filePath: path.resolve(__dirname, 'src/get-random-values-polyfill.js'),
type: 'sourceFile',
};
}
// Intercept @segment/sovran-react-native — prevents NativeModules
// BatchedBridge crash at module-load time.
if (moduleName === '@segment/sovran-react-native' ||
moduleName.endsWith('/sovran-react-native')) {
return {
filePath: path.resolve(__dirname, 'src/sovran-polyfill.js'),
type: 'sourceFile',
};
}
// Intercept analytics-react-native's context.ts — prevents
// AnalyticsReactNative native module lookup crash.
if (moduleName === '@segment/analytics-react-native/src/context' ||
((moduleName === './context' || moduleName.endsWith('/context')) &&
context.originModulePath.includes('analytics-react-native'))) {
return {
filePath: path.resolve(__dirname, 'src/context-polyfill.js'),
type: 'sourceFile',
};
}
// Chain to Kepler's resolver — required for the Kepler runtime bundle.
if (keplerResolveRequest) {
return keplerResolveRequest(context, moduleName, platform);
}
return context.resolveRequest(context, moduleName, platform);
},
},
transformer: {
transformIgnorePatterns: [
'node_modules/(?!(@amazon-devices|@amzn|@segment|react-native|@react-native|@react-native-async-storage)/)',
],
},
};

module.exports = mergeConfig(defaultConfig, config);
```

#### Why these shims are needed

Kepler 4's `react-native` bundle exposes `NativeModules` as a lazy getter that loads `BatchedBridge/NativeModules.js` on first access. That module unconditionally throws `__fbBatchedBridgeConfig is not set` because Kepler 4 has no legacy bridge. This throw propagates through Metro's internal module-loading machinery and cannot be caught by a try/catch in user code — it must be prevented at the resolver level before Metro ever loads the offending module.

Now create you client as follows:

```ts
Expand Down
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
presets: ['module:@react-native/babel-preset'],
};
11 changes: 11 additions & 0 deletions example-83/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": [
"@react-native",
"eslint:recommended"
],
"plugins": ["@typescript-eslint"],
"parserOptions": {
"project": "./tsconfig.json"
},
"parser": "@typescript-eslint/parser"
}
13 changes: 13 additions & 0 deletions example-83/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
*.iml
*.swp
.idea
.DS_Store
.vscode
.tmp

dist
build
node_modules
buildinfo.json
CMakeUserPresets.json
npm-debug.log
2 changes: 2 additions & 0 deletions example-83/.mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tools]
node = "18"
7 changes: 7 additions & 0 deletions example-83/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"bracketSameLine": true,
"bracketSpacing": false,
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2
}
15 changes: 15 additions & 0 deletions example-83/analytics-react-native-plugin-kepler.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"folders": [
{
"path": ".."
},
{
"path": "."
}
],
"settings": {
"C_Cpp.default.includePath": [
"${workspaceFolder}/**"
]
}
}
5 changes: 5 additions & 0 deletions example-83/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"//": "The declared app name must follow this pattern to be KRL-compatible",
"name": "com.segment.keplersample83.main",
"displayName": "AmazonKepler83"
}
8 changes: 8 additions & 0 deletions example-83/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright (c) 2022 Amazon.com, Inc. or its affiliates. All rights reserved.
*
* PROPRIETARY/CONFIDENTIAL. USE IS SUBJECT TO LICENSE TERMS.
*/
module.exports = {
presets: ['module:@react-native/babel-preset'],
};
15 changes: 15 additions & 0 deletions example-83/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2022 Amazon.com, Inc. or its affiliates. All rights reserved.
*
* PROPRIETARY/CONFIDENTIAL. USE IS SUBJECT TO LICENSE TERMS.
*/

import { AppRegistry, LogBox } from 'react-native';
import { App } from './src/App';
import { name as appName } from './app.json';

// Temporary workaround for problem with nested text
// not working currently.
LogBox.ignoreAllLogs();

AppRegistry.registerComponent(appName, () => App);
35 changes: 35 additions & 0 deletions example-83/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"preset": "react-native",
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "/tst/.*\\.(test|spec)\\.(ts|tsx|js)$",
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
"moduleNameMapper": {
"^~/(.*)$": "<rootDir>/src/$1"
},
"modulePaths": ["<rootDir>/src"],
"roots": [
"<rootDir>/src/",
"<rootDir>/tst/",
"<rootDir>/node_modules/"
],
"collectCoverage": false,
"coverageReporters": ["json", "json-summary", "lcov", "text"],
"collectCoverageFrom": [
"src/**/*",
"!**/index.{ts,js,tsx}",
"!**/types.ts",
"!src/**/*.d.ts"
],
"transformIgnorePatterns": [
"node_modules/(?!(@amazon-devices|react-native|@react-native|@react-native-async-storage)/)"
],
"coverageDirectory": ".tmp/coverage",
"globals": {
"ts-jest": {
"babelConfig": true,
"diagnostics": true
}
}
}
18 changes: 18 additions & 0 deletions example-83/manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# Copyright (c) 2022 Amazon.com, Inc. or its affiliates. All rights reserved.
#
# PROPRIETARY/CONFIDENTIAL. USE IS SUBJECT TO LICENSE TERMS.
#
schema-version = 1

[package]
title = "Basic UI React Native Application"
version = "0.1.0"
id = "com.segment.keplersample83"

[components]
[[components.interactive]]
id = "com.segment.keplersample83.main"
runtime-module = "/com.amazon.kepler.runtime.react_native_kepler_4@IReactNativeKepler_0"
categories = ["com.amazon.category.main"]
launch-type = "singleton"
78 changes: 78 additions & 0 deletions example-83/metro.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2022 Amazon.com, Inc. or its affiliates. All rights reserved.
*
* PROPRIETARY/CONFIDENTIAL. USE IS SUBJECT TO LICENSE TERMS.
*/

const path = require('path');
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

/**
* Metro configuration
* https://facebook.github.io/metro/docs/configuration
*
* @type {import('metro-config').MetroConfig}
*/
const defaultConfig = getDefaultConfig(__dirname);

// Capture Kepler's resolveRequest before merging — it remaps `react-native`
// to the @amzn/react-native-kepler system bundle. Must chain to it.
const keplerResolveRequest = defaultConfig.resolver?.resolveRequest;

const config = {
resolver: {
platforms: [...(defaultConfig.resolver?.platforms ?? []), 'kepler'],
extraNodeModules: {
// @amazon-devices/react-native-kepler still embeds @amzn/* source refs
// internally; alias the old name so Metro resolves assets correctly.
'@amzn/react-native-kepler': path.resolve(
__dirname,
'node_modules/@amazon-devices/react-native-kepler',
),
},
resolveRequest: (context, moduleName, platform) => {
// react-native-get-random-values calls TurboModuleRegistry.getEnforcing
// for RNGetRandomValues which is not registered on Kepler 4. Replace with
// a pure-JS Math.random polyfill.
if (moduleName === 'react-native-get-random-values') {
return {
filePath: path.resolve(__dirname, 'src/get-random-values-polyfill.js'),
type: 'sourceFile',
};
}
// @segment/sovran-react-native index.tsx accesses NativeModules which
// triggers Kepler's legacy BatchedBridge and crashes before any try/catch.
if (moduleName === '@segment/sovran-react-native' ||
moduleName.endsWith('/sovran-react-native')) {
return {
filePath: path.resolve(__dirname, 'src/sovran-polyfill.js'),
type: 'sourceFile',
};
}
// context.ts calls getNativeModule('AnalyticsReactNative') which also
// triggers NativeModules. Kepler plugin provides its own deviceInfoProvider
// so returning defaultContext here is safe.
// Match both absolute package path and relative imports (./context, ../context)
// from within the analytics-react-native package.
if (moduleName === '@segment/analytics-react-native/src/context' ||
((moduleName === './context' || moduleName.endsWith('/context')) &&
context.originModulePath.includes('analytics-react-native'))) {
return {
filePath: path.resolve(__dirname, 'src/context-polyfill.js'),
type: 'sourceFile',
};
}
if (keplerResolveRequest) {
return keplerResolveRequest(context, moduleName, platform);
}
return context.resolveRequest(context, moduleName, platform);
},
},
transformer: {
transformIgnorePatterns: [
'node_modules/(?!(@amazon-devices|@amzn|@segment|react-native|@react-native|@react-native-async-storage)/)',
],
},
};

module.exports = mergeConfig(defaultConfig, config);
Loading