Skip to content

feat: add react native web support #13

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ If you're targeting Android, you'll need to add the following permissions to you
<uses-permission android:name="android.permission.INTERNET" />
```

## Web Support

This SDK also supports React Native Web!

> [!NOTE]
> This feature is disabled by default. To enable it, you need to pass the `enableWeb` option when initializing the SDK.

```js
Aptabase.init("<YOUR_APP_KEY>", { enableWeb: true });
```

When enabled, the SDK will track events in web environments using the same behavior as the web SDKs. Which means that events will be sent immediately to the `/event` endpoint instead of grouped to the `/events` endpoint.

## Usage

First, you need to get your `App Key` from Aptabase, you can find it in the `Instructions` menu on the left side menu.
Expand Down Expand Up @@ -64,7 +77,9 @@ export function Counter() {
);
}
```

To disable tracking events, you can call the `dispose` function. This will stop and deinitalize the SDK.

```js
import Aptabase from "@aptabase/react-native";

Expand Down
3,186 changes: 1,796 additions & 1,390 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
"aptabase-react-native.podspec"
],
"devDependencies": {
"@vitest/coverage-v8": "0.34.3",
"@types/react": "18.2.22",
"@types/node": "20.5.9",
"tsup": "7.2.0",
"vite": "4.4.9",
"vitest": "0.34.3",
"vitest-fetch-mock": "0.2.2"
"@types/node": "22.15.21",
"@types/react": "19.1.5",
"@vitest/coverage-v8": "3.1.4",
"tsup": "8.5.0",
"vite": "6.3.5",
"vitest": "3.1.4",
"vitest-fetch-mock": "0.4.5"
},
"peerDependencies": {
"react": "*",
Expand Down
75 changes: 75 additions & 0 deletions src/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,79 @@ describe("AptabaseClient", () => {
expect(sessionId3).toBeDefined();
expect(sessionId3).not.toBe(sessionId1);
});

describe("Web tracking", () => {
const webEnv: EnvironmentInfo = {
...env,
osName: "web",
osVersion: "web",
};

it("should not track events when web tracking is disabled", async () => {
const client = new AptabaseClient("A-DEV-000", webEnv);
client.trackEvent("test_event");
await client.flush();
expect(fetchMock.requests().length).toEqual(0);
});

it("should track events when web tracking is enabled", async () => {
const client = new AptabaseClient("A-DEV-000", webEnv, {
enableWeb: true,
});
client.trackEvent("test_event");
await client.flush();
expect(fetchMock.requests().length).toEqual(1);
const body = await fetchMock.requests().at(0)?.json();
expect(body.eventName).toEqual("test_event");
expect(body.systemProps.osName).toBeUndefined();
expect(body.systemProps.osVersion).toBeUndefined();
});

it("should use correct endpoint for web events", async () => {
const client = new AptabaseClient("A-DEV-000", webEnv, {
enableWeb: true,
});
client.trackEvent("test_event");
await client.flush();
const request = fetchMock.requests().at(0);
expect(request?.url).toContain("/api/v0/event");
});

it("should use correct endpoint for native events", async () => {
const client = new AptabaseClient("A-DEV-000", env);
client.trackEvent("test_event");
await client.flush();
const request = fetchMock.requests().at(0);
expect(request?.url).toContain("/api/v0/events");
});
});

describe("Native tracking", () => {
it("should track events on iOS", async () => {
const client = new AptabaseClient("A-DEV-000", env);
client.trackEvent("test_event");
await client.flush();
expect(fetchMock.requests().length).toEqual(1);
const body = await fetchMock.requests().at(0)?.json();
expect(body[0].eventName).toEqual("test_event");
expect(body[0].systemProps.osName).toEqual("iOS");
expect(body[0].systemProps.osVersion).toEqual("14.3");
});

it("should track events on Android", async () => {
const androidEnv: EnvironmentInfo = {
...env,
osName: "Android",
osVersion: "13",
};
const client = new AptabaseClient("A-DEV-000", androidEnv);
client.trackEvent("test_event");
await client.flush();
expect(fetchMock.requests().length).toEqual(1);
const body = await fetchMock.requests().at(0)?.json();
expect(body[0].eventName).toEqual("test_event");
expect(body[0].systemProps.osName).toEqual("Android");
expect(body[0].systemProps.osVersion).toEqual("13");
});
});
});
29 changes: 23 additions & 6 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { Platform } from "react-native";
import type { AptabaseOptions } from "./types";
import type { EnvironmentInfo } from "./env";
import { EventDispatcher } from "./dispatcher";
import { NativeEventDispatcher, WebEventDispatcher } from "./dispatcher";
import { newSessionId } from "./session";
import { HOSTS, SESSION_TIMEOUT } from "./constants";

export class AptabaseClient {
private readonly _dispatcher: EventDispatcher;
private readonly _dispatcher:
| WebEventDispatcher
| NativeEventDispatcher
| null;
private readonly _env: EnvironmentInfo;
private _sessionId = newSessionId();
private _lastTouched = new Date();
Expand All @@ -21,22 +23,36 @@ export class AptabaseClient {
this._env.appVersion = options.appVersion;
}

this._dispatcher = new EventDispatcher(appKey, baseUrl, env);
const isWeb = this._env.osName === "web";
const isWebTrackingEnabled = isWeb && options?.enableWeb === true;

const shouldEnableTracking = !isWeb || isWebTrackingEnabled;
const dispatcher = shouldEnableTracking
? isWeb
? new WebEventDispatcher(appKey, baseUrl, env)
: new NativeEventDispatcher(appKey, baseUrl, env)
: null;

this._dispatcher = dispatcher;
}

public trackEvent(
eventName: string,
props?: Record<string, string | number | boolean>
) {
if (!this._dispatcher) return;

const isWeb = this._env.osName === "web";

this._dispatcher.enqueue({
timestamp: new Date().toISOString(),
sessionId: this.evalSessionId(),
eventName: eventName,
systemProps: {
isDebug: this._env.isDebug,
locale: this._env.locale,
osName: this._env.osName,
osVersion: this._env.osVersion,
osName: isWeb ? undefined : this._env.osName,
osVersion: isWeb ? undefined : this._env.osVersion,
appVersion: this._env.appVersion,
appBuildNumber: this._env.appBuildNumber,
sdkVersion: this._env.sdkVersion,
Expand All @@ -59,6 +75,7 @@ export class AptabaseClient {
}

public flush(): Promise<void> {
if (!this._dispatcher) return Promise.resolve();
return this._dispatcher.flush();
}

Expand Down
70 changes: 65 additions & 5 deletions src/dispatcher.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "vitest-fetch-mock";
import { EventDispatcher } from "./dispatcher";
import { NativeEventDispatcher, WebEventDispatcher } from "./dispatcher";
import { beforeEach, describe, expect, it } from "vitest";
import { EnvironmentInfo } from "./env";
import type { EnvironmentInfo } from "./env";

const env: EnvironmentInfo = {
isDebug: false,
Expand Down Expand Up @@ -32,11 +32,11 @@ const expectEventsCount = async (
expect(body.length).toEqual(expectedNumOfEvents);
};

describe("EventDispatcher", () => {
let dispatcher: EventDispatcher;
describe("NativeEventDispatcher", () => {
let dispatcher: NativeEventDispatcher;

beforeEach(() => {
dispatcher = new EventDispatcher(
dispatcher = new NativeEventDispatcher(
"A-DEV-000",
"https://localhost:3000",
env
Expand Down Expand Up @@ -138,3 +138,63 @@ describe("EventDispatcher", () => {
expectRequestCount(1);
});
});

describe("WebEventDispatcher", () => {
let dispatcher: WebEventDispatcher;

beforeEach(() => {
dispatcher = new WebEventDispatcher(
"A-DEV-000",
"https://localhost:3000",
env
);
fetchMock.resetMocks();
});

it("should send event with correct headers", async () => {
dispatcher.enqueue(createEvent("app_started"));

const request = await fetchMock.requests().at(0);
expect(request).not.toBeUndefined();
expect(request?.url).toEqual("https://localhost:3000/api/v0/event");
expect(request?.headers.get("Content-Type")).toEqual("application/json");
expect(request?.headers.get("App-Key")).toEqual("A-DEV-000");
});

it("should dispatch single event", async () => {
fetchMock.mockResponseOnce("{}");

dispatcher.enqueue(createEvent("app_started"));

expectRequestCount(1);
const body = await fetchMock.requests().at(0)?.json();
expect(body.eventName).toEqual("app_started");
});

it("should dispatch multiple events individually", async () => {
fetchMock.mockResponseOnce("{}");
fetchMock.mockResponseOnce("{}");

dispatcher.enqueue([createEvent("app_started"), createEvent("app_exited")]);

expectRequestCount(2);
const body1 = await fetchMock.requests().at(0)?.json();
const body2 = await fetchMock.requests().at(1)?.json();
expect(body1.eventName).toEqual("app_started");
expect(body2.eventName).toEqual("app_exited");
});

it("should not retry requests that failed with 4xx", async () => {
fetchMock.mockResponseOnce("{}", { status: 400 });

dispatcher.enqueue(createEvent("hello_world"));

expectRequestCount(1);
const body = await fetchMock.requests().at(0)?.json();
expect(body.eventName).toEqual("hello_world");

dispatcher.enqueue(createEvent("hello_world"));

expectRequestCount(2);
});
});
Loading