Skip to content

fix: add support for the React Native environment. #223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 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
10 changes: 5 additions & 5 deletions e2e/BKTClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import { BKTClient, BKTClientImpl } from '../src/BKTClient'
import { DefaultComponent } from '../src/internal/di/Component'
import { EvaluationStorageImpl } from '../src/internal/evaluation/EvaluationStorage'
import { evaluation1 } from '../test/mocks/evaluations'
import { fetchLike } from './environment'

suite('e2e/BKTClientTest', () => {
let config: BKTConfig
let user: BKTUser

afterEach(() => {
destroyBKTClient()
localStorage.clear()
})

suite('get string variation using user attribute when initializing', () => {
Expand All @@ -29,7 +29,7 @@ suite('e2e/BKTClientTest', () => {
apiKey: import.meta.env.VITE_BKT_API_KEY,
featureTag: 'javascript',
appVersion: '1.2.3',
fetch: window.fetch,
fetch: fetchLike,
})

user = defineBKTUser({
Expand All @@ -54,7 +54,7 @@ suite('e2e/BKTClientTest', () => {
apiKey: import.meta.env.VITE_BKT_API_KEY,
featureTag: 'javascript',
appVersion: '1.2.3',
fetch: window.fetch,
fetch: fetchLike,
})

user = defineBKTUser({
Expand Down Expand Up @@ -99,7 +99,7 @@ suite('e2e/BKTClientTest', () => {
apiKey: import.meta.env.VITE_BKT_API_KEY,
featureTag: 'javascript',
appVersion: '1.2.3',
fetch: window.fetch,
fetch: fetchLike,
})

user = defineBKTUser({
Expand Down Expand Up @@ -201,7 +201,7 @@ suite('e2e/BKTClientTest', () => {
apiEndpoint: import.meta.env.VITE_BKT_API_ENDPOINT,
apiKey: import.meta.env.VITE_BKT_API_KEY,
appVersion: '1.2.3',
fetch: window.fetch,
fetch: fetchLike,
})

user = defineBKTUser({
Expand Down
19 changes: 19 additions & 0 deletions e2e/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FetchLike } from '../src/internal/remote/fetch'

let fetchLike: FetchLike
let isNodeEnvironment = false

function setFetchProvider(fetch: FetchLike) {
fetchLike = fetch
}

function setIsNodeEnvironment(isNode: boolean) {
isNodeEnvironment = isNode
}

export {
setFetchProvider,
setIsNodeEnvironment,
fetchLike,
isNodeEnvironment,
}
4 changes: 2 additions & 2 deletions e2e/evaluations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
USER_ID,
} from './constants'
import './assertions'
import { fetchLike } from './environment'

suite('e2e/evaluations', () => {
let config: BKTConfig
Expand All @@ -26,7 +27,7 @@ suite('e2e/evaluations', () => {
apiKey: import.meta.env.VITE_BKT_API_KEY,
featureTag: 'javascript',
appVersion: '1.2.3',
fetch: window.fetch,
fetch: fetchLike,
})

user = defineBKTUser({
Expand All @@ -38,7 +39,6 @@ suite('e2e/evaluations', () => {

afterEach(() => {
destroyBKTClient()
localStorage.clear()
})

suite('stringVariation', () => {
Expand Down
59 changes: 31 additions & 28 deletions e2e/events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import './assertions'
import { EventType } from '../src/internal/model/Event'
import { ForbiddenException, TimeoutException } from '../src/BKTExceptions'
import { ApiId, MetricsEventType } from '../src/internal/model/MetricsEventData'
import { fetchLike, isNodeEnvironment } from './environment'

function getDefaultComponent(client: BKTClient): DefaultComponent {
return (client as BKTClientImpl).component as DefaultComponent
Expand All @@ -38,7 +39,7 @@ suite('e2e/events', () => {
apiKey: import.meta.env.VITE_BKT_API_KEY,
featureTag: 'javascript',
appVersion: '1.2.3',
fetch: window.fetch,
fetch: fetchLike,
// DO NOT remove this line
// Because the tests are asynchronous and share the same local storage,
// It might fail randomly, having more or fewer events in the storage when checking the test.
Expand All @@ -55,7 +56,6 @@ suite('e2e/events', () => {

afterEach(() => {
destroyBKTClient()
localStorage.clear()
})

test('goal event', async () => {
Expand Down Expand Up @@ -174,14 +174,13 @@ suite('e2e/events', () => {

test('Using a random string in the api key setting should throw Forbidden', async () => {
destroyBKTClient()
localStorage.clear()

config = defineBKTConfig({
apiEndpoint: import.meta.env.VITE_BKT_API_ENDPOINT,
apiKey: 'some-random-string',
featureTag: 'javascript',
appVersion: '1.2.3',
fetch: window.fetch,
fetch: fetchLike,
})

user = defineBKTUser({
Expand Down Expand Up @@ -224,7 +223,7 @@ suite('e2e/events', () => {
apiKey: import.meta.env.VITE_BKT_API_KEY,
featureTag: 'javascript',
appVersion: '1.2.3',
fetch: window.fetch,
fetch: fetchLike,
})

await initializeBKTClient(config, user)
Expand All @@ -234,37 +233,43 @@ suite('e2e/events', () => {
const component2 = getDefaultComponent(client)

const events3 = component2.dataModule.eventStorage().getAll()
// 2 events - latency and response size
expect(events3).toHaveLength(2)
// ForbiddenError should not exist
expect(
events.some((e) => {
return (
e.type === EventType.METRICS &&
e.event.event['@type'] === MetricsEventType.ForbiddenError &&
e.event.event.apiId === ApiId.GET_EVALUATIONS
)
}),
).toBe(false)

await client2.flush()

const events4 = component2.dataModule.eventStorage().getAll()

// error from /register_events does not get stored
expect(events4).toHaveLength(0)
if (isNodeEnvironment) {
// on the node environment, no events should be stored after destroying the client
// because its using in-memory storage
Copy link
Preview

Copilot AI Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The inline comment on the following line uses 'its' instead of "it's"; a small grammar fix would improve clarity.

Suggested change
// because its using in-memory storage
// because it's using in-memory storage

Copilot uses AI. Check for mistakes.

expect(events3).toHaveLength(0)
} else {
// on the browser environment, we should have 2 events - latency and response size
expect(events3).toHaveLength(2)
// ForbiddenError should not exist
expect(
events.some((e) => {
return (
e.type === EventType.METRICS &&
e.event.event['@type'] === MetricsEventType.ForbiddenError &&
e.event.event.apiId === ApiId.GET_EVALUATIONS
)
}),
).toBe(false)

await client2.flush()

const events4 = component2.dataModule.eventStorage().getAll()

// error from /register_events does not get stored
expect(events4).toHaveLength(0)
}
})

test('Using a random string in the featureTag setting should not affect api request', async () => {
destroyBKTClient()
localStorage.clear()

config = defineBKTConfig({
apiEndpoint: import.meta.env.VITE_BKT_API_ENDPOINT,
apiKey: import.meta.env.VITE_BKT_API_KEY,
featureTag: 'some-random-feature-tag',
appVersion: '1.2.3',
fetch: window.fetch,
fetch: fetchLike,
})

user = defineBKTUser({
Expand All @@ -276,16 +281,14 @@ suite('e2e/events', () => {

test('Timeout', async () => {
// setting a very low value for the timeout

destroyBKTClient()
localStorage.clear()

config = defineBKTConfig({
apiEndpoint: import.meta.env.VITE_BKT_API_ENDPOINT,
apiKey: import.meta.env.VITE_BKT_API_KEY,
featureTag: 'javascript',
appVersion: '1.2.3',
fetch: window.fetch,
fetch: fetchLike,
})

user = defineBKTUser({
Expand Down
9 changes: 9 additions & 0 deletions e2e/setup.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

import { afterEach } from 'vitest'
import { setFetchProvider } from './environment'

setFetchProvider(window.fetch)

afterEach(() => {
localStorage.clear()
})
5 changes: 5 additions & 0 deletions e2e/setup.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { setFetchProvider, setIsNodeEnvironment } from './environment'

setFetchProvider(fetch)

setIsNodeEnvironment(true)
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@
"module": "./dist/main.mjs",
"browser": "./dist/main.browser.mjs",
"types": "./dist/main.d.ts",
"react-native": "./dist/main.native.cjs",
"exports": {
".": {
"node": {
"import": "./dist/main.mjs",
"require": "./dist/main.cjs",
"types": "./dist/main.d.ts"
},
"react-native": {
"import": "./dist/main.native.mjs",
"require": "./dist/main.native.cjs",
"types": "./dist/main.d.ts"
},
"default": {
"import": "./dist/main.browser.mjs",
"require": "./dist/main.browser.cjs",
Expand All @@ -35,7 +41,9 @@
"test": "pnpm test:browser --run ; pnpm test:node --run",
"test:browser": "vitest --config ./vitest-browser.config.ts --dir test",
"test:node": "vitest --config ./vitest-node.config.ts --dir test",
"test:e2e": "vitest --config ./vitest-e2e.config.ts --dir e2e",
"test:e2e": "pnpm test:e2e:browser --run ; pnpm test:e2e:node --run",
"test:e2e:browser": "vitest --config ./vitest-e2e.config.ts --dir e2e",
"test:e2e:node": "vitest --config ./vitest-e2e-node.config.ts --dir e2e",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"example:serve": "pnpm exec unbuild && pnpm --filter example serve"
Expand Down
2 changes: 2 additions & 0 deletions src/BKTConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IllegalArgumentException } from './BKTExceptions'
import { BKTStorage, createBKTStorage } from './BKTStorage'
import { IdGenerator } from './internal/IdGenerator'
import { FetchLike } from './internal/remote/fetch'
import { SDK_VERSION } from './internal/version'

Expand Down Expand Up @@ -30,6 +31,7 @@ interface RawBKTConfig {
userAgent?: string
fetch?: FetchLike
storageFactory?: <T>(key: string) => BKTStorage<T>
idGenerator?: IdGenerator
}

export interface BKTConfig extends RawBKTConfig {
Expand Down
14 changes: 14 additions & 0 deletions src/internal/di/PlatformModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,17 @@ import { IdGenerator } from '../IdGenerator'
export interface PlatformModule {
idGenerator(): IdGenerator
}

// BasePlatformModule serves as a base implementation of the PlatformModule interface.
// It relies on the BKTConfig to inject an instance of IdGenerator, which must be set before use.
export class BasePlatformModule implements PlatformModule {
protected _idGenerator: IdGenerator

constructor(params: { idGenerator: IdGenerator }) {
this._idGenerator = params.idGenerator
}

idGenerator() {
return this._idGenerator
}
}
56 changes: 56 additions & 0 deletions src/main.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { initializeBKTClientInternal } from './BKTClient'
import { BKTConfig } from './BKTConfig'
import { BKTUser } from './BKTUser'
import { Component, DefaultComponent } from './internal/di/Component'
import { DataModule } from './internal/di/DataModule'
import { InteractorModule } from './internal/di/InteractorModule'
import { BasePlatformModule } from './internal/di/PlatformModule'
import { User } from './internal/model/User'
import { toUser } from './internal/UserHolder'
import { IdGenerator } from './internal/IdGenerator'

export type { BKTConfig } from './BKTConfig'
export { defineBKTConfig } from './BKTConfig'
export type { BKTUser } from './BKTUser'
export { defineBKTUser } from './BKTUser'
export type { BKTClient } from './BKTClient'
export { getBKTClient, destroyBKTClient } from './BKTClient'
export type {
BKTStorage,
BrowserLocalStorage,
InMemoryStorage,
} from './BKTStorage'
export type {
BKTValue,
BKTJsonArray,
BKTJsonObject,
BKTJsonPrimitive,
} from './BKTValue'
export type { BKTEvaluationDetails } from './BKTEvaluationDetails'

// This endpoint is intended for use in React Native - Expo environments.
const createComponent = (config: BKTConfig, user: User): Component => {
const idGenerator = requiredIdGenerator(config)
return new DefaultComponent(
new BasePlatformModule({ idGenerator }),
new DataModule(user, config),
new InteractorModule(),
)
}

export const initializeBKTClient = async (
config: BKTConfig,
user: BKTUser,
timeoutMillis = 5_000,
): Promise<void> => {
// idGenerator is required in the react native environment
const component = createComponent(config, toUser(user))
return initializeBKTClientInternal(component, timeoutMillis)
}

export function requiredIdGenerator(config: BKTConfig): IdGenerator {
if (!config.idGenerator) {
throw new Error('idGenerator is required in this environment')
}
return config.idGenerator
}
5 changes: 4 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BKTUser } from './BKTUser'
import { Component, DefaultComponent } from './internal/di/Component'
import { DataModule } from './internal/di/DataModule'
import { InteractorModule } from './internal/di/InteractorModule'
import { BasePlatformModule } from './internal/di/PlatformModule'
import { NodePlatformModule } from './internal/di/PlatformModule.node'
import { User } from './internal/model/User'
import { toUser } from './internal/UserHolder'
Expand All @@ -29,7 +30,9 @@ export type { BKTEvaluationDetails } from './BKTEvaluationDetails'

const createNodeComponent = (config: BKTConfig, user: User): Component => {
return new DefaultComponent(
new NodePlatformModule(),
config.idGenerator
? new BasePlatformModule({ idGenerator: config.idGenerator })
: new NodePlatformModule(),
new DataModule(user, config),
new InteractorModule(),
)
Expand Down
Loading