Skip to content

Commit fa07ed6

Browse files
authored
Merge pull request #13 from Robert27/feat/react-native-web
feat: add react native web support
2 parents 93001af + 4903197 commit fa07ed6

10 files changed

Lines changed: 321 additions & 57 deletions

File tree

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ If you're targeting Android, you'll need to add the following permissions to you
2020
<uses-permission android:name="android.permission.INTERNET" />
2121
```
2222

23+
## Web Support
24+
25+
This SDK also supports React Native Web!
26+
27+
> [!NOTE]
28+
> This feature is disabled by default. To enable it, you need to pass the `enableWeb` option when initializing the SDK.
29+
30+
```js
31+
Aptabase.init("<YOUR_APP_KEY>", {
32+
enableWeb: true,
33+
appVersion: "1.0.0", // required on web — no native module provides it
34+
});
35+
```
36+
37+
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.
38+
2339
## Usage
2440

2541
First, you need to get your `App Key` from Aptabase, you can find it in the `Instructions` menu on the left side menu.
@@ -64,7 +80,9 @@ export function Counter() {
6480
);
6581
}
6682
```
67-
To disable tracking events, you can call the `dispose` function. This will stop and deinitalize the SDK.
83+
84+
To disable tracking events, you can call the `dispose` function. This will stop and deinitialize the SDK.
85+
6886
```js
6987
import Aptabase from "@aptabase/react-native";
7088

src/client.spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,79 @@ describe("AptabaseClient", () => {
119119
expect(sessionId3).toBeDefined();
120120
expect(sessionId3).not.toBe(sessionId1);
121121
});
122+
123+
describe("Web tracking", () => {
124+
const webEnv: EnvironmentInfo = {
125+
...env,
126+
osName: "web",
127+
osVersion: "web",
128+
};
129+
130+
it("should not track events when web tracking is disabled", async () => {
131+
const client = new AptabaseClient("A-DEV-000", webEnv);
132+
client.trackEvent("test_event");
133+
await client.flush();
134+
expect(fetchMock.requests().length).toEqual(0);
135+
});
136+
137+
it("should track events when web tracking is enabled", async () => {
138+
const client = new AptabaseClient("A-DEV-000", webEnv, {
139+
enableWeb: true,
140+
});
141+
client.trackEvent("test_event");
142+
await client.flush();
143+
expect(fetchMock.requests().length).toEqual(1);
144+
const body = await fetchMock.requests().at(0)?.json();
145+
expect(body.eventName).toEqual("test_event");
146+
expect(body.systemProps.osName).toBeUndefined();
147+
expect(body.systemProps.osVersion).toBeUndefined();
148+
});
149+
150+
it("should use correct endpoint for web events", async () => {
151+
const client = new AptabaseClient("A-DEV-000", webEnv, {
152+
enableWeb: true,
153+
});
154+
client.trackEvent("test_event");
155+
await client.flush();
156+
const request = fetchMock.requests().at(0);
157+
expect(request?.url).toContain("/api/v0/event");
158+
});
159+
160+
it("should use correct endpoint for native events", async () => {
161+
const client = new AptabaseClient("A-DEV-000", env);
162+
client.trackEvent("test_event");
163+
await client.flush();
164+
const request = fetchMock.requests().at(0);
165+
expect(request?.url).toContain("/api/v0/events");
166+
});
167+
});
168+
169+
describe("Native tracking", () => {
170+
it("should track events on iOS", async () => {
171+
const client = new AptabaseClient("A-DEV-000", env);
172+
client.trackEvent("test_event");
173+
await client.flush();
174+
expect(fetchMock.requests().length).toEqual(1);
175+
const body = await fetchMock.requests().at(0)?.json();
176+
expect(body[0].eventName).toEqual("test_event");
177+
expect(body[0].systemProps.osName).toEqual("iOS");
178+
expect(body[0].systemProps.osVersion).toEqual("14.3");
179+
});
180+
181+
it("should track events on Android", async () => {
182+
const androidEnv: EnvironmentInfo = {
183+
...env,
184+
osName: "Android",
185+
osVersion: "13",
186+
};
187+
const client = new AptabaseClient("A-DEV-000", androidEnv);
188+
client.trackEvent("test_event");
189+
await client.flush();
190+
expect(fetchMock.requests().length).toEqual(1);
191+
const body = await fetchMock.requests().at(0)?.json();
192+
expect(body[0].eventName).toEqual("test_event");
193+
expect(body[0].systemProps.osName).toEqual("Android");
194+
expect(body[0].systemProps.osVersion).toEqual("13");
195+
});
196+
});
122197
});

src/client.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import type { Platform } from "react-native";
21
import type { AptabaseOptions } from "./types";
32
import type { EnvironmentInfo } from "./env";
4-
import { EventDispatcher } from "./dispatcher";
3+
import { NativeEventDispatcher, WebEventDispatcher } from "./dispatcher";
54
import { newSessionId } from "./session";
65
import { HOSTS, SESSION_TIMEOUT } from "./constants";
76

87
export class AptabaseClient {
9-
private readonly _dispatcher: EventDispatcher;
8+
private readonly _dispatcher:
9+
| WebEventDispatcher
10+
| NativeEventDispatcher
11+
| null;
1012
private readonly _env: EnvironmentInfo;
1113
private _sessionId = newSessionId();
1214
private _lastTouched = new Date();
@@ -21,22 +23,36 @@ export class AptabaseClient {
2123
this._env.appVersion = options.appVersion;
2224
}
2325

24-
this._dispatcher = new EventDispatcher(appKey, baseUrl, env);
26+
const isWeb = this._env.osName === "web";
27+
const isWebTrackingEnabled = isWeb && options?.enableWeb === true;
28+
29+
const shouldEnableTracking = !isWeb || isWebTrackingEnabled;
30+
const dispatcher = shouldEnableTracking
31+
? isWeb
32+
? new WebEventDispatcher(appKey, baseUrl, env)
33+
: new NativeEventDispatcher(appKey, baseUrl, env)
34+
: null;
35+
36+
this._dispatcher = dispatcher;
2537
}
2638

2739
public trackEvent(
2840
eventName: string,
2941
props?: Record<string, string | number | boolean>
3042
) {
43+
if (!this._dispatcher) return;
44+
45+
const isWeb = this._env.osName === "web";
46+
3147
this._dispatcher.enqueue({
3248
timestamp: new Date().toISOString(),
3349
sessionId: this.evalSessionId(),
3450
eventName: eventName,
3551
systemProps: {
3652
isDebug: this._env.isDebug,
3753
locale: this._env.locale,
38-
osName: this._env.osName,
39-
osVersion: this._env.osVersion,
54+
osName: isWeb ? undefined : this._env.osName,
55+
osVersion: isWeb ? undefined : this._env.osVersion,
4056
appVersion: this._env.appVersion,
4157
appBuildNumber: this._env.appBuildNumber,
4258
sdkVersion: this._env.sdkVersion,
@@ -46,6 +62,10 @@ export class AptabaseClient {
4662
}
4763

4864
public startPolling(flushInterval: number) {
65+
if (!(this._dispatcher instanceof NativeEventDispatcher)) {
66+
return;
67+
}
68+
4969
this.stopPolling();
5070

5171
this._flushTimer = setInterval(this.flush.bind(this), flushInterval);
@@ -59,6 +79,7 @@ export class AptabaseClient {
5979
}
6080

6181
public flush(): Promise<void> {
82+
if (!this._dispatcher) return Promise.resolve();
6283
return this._dispatcher.flush();
6384
}
6485

src/dispatcher.spec.ts

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "vitest-fetch-mock";
2-
import { EventDispatcher } from "./dispatcher";
2+
import { NativeEventDispatcher, WebEventDispatcher } from "./dispatcher";
33
import { beforeEach, describe, expect, it } from "vitest";
4-
import { EnvironmentInfo } from "./env";
4+
import type { EnvironmentInfo } from "./env";
55

66
const env: EnvironmentInfo = {
77
isDebug: false,
@@ -32,11 +32,11 @@ const expectEventsCount = async (
3232
expect(body.length).toEqual(expectedNumOfEvents);
3333
};
3434

35-
describe("EventDispatcher", () => {
36-
let dispatcher: EventDispatcher;
35+
describe("NativeEventDispatcher", () => {
36+
let dispatcher: NativeEventDispatcher;
3737

3838
beforeEach(() => {
39-
dispatcher = new EventDispatcher(
39+
dispatcher = new NativeEventDispatcher(
4040
"A-DEV-000",
4141
"https://localhost:3000",
4242
env
@@ -138,3 +138,63 @@ describe("EventDispatcher", () => {
138138
expectRequestCount(1);
139139
});
140140
});
141+
142+
describe("WebEventDispatcher", () => {
143+
let dispatcher: WebEventDispatcher;
144+
145+
beforeEach(() => {
146+
dispatcher = new WebEventDispatcher(
147+
"A-DEV-000",
148+
"https://localhost:3000",
149+
env
150+
);
151+
fetchMock.resetMocks();
152+
});
153+
154+
it("should send event with correct headers", async () => {
155+
dispatcher.enqueue(createEvent("app_started"));
156+
157+
const request = await fetchMock.requests().at(0);
158+
expect(request).not.toBeUndefined();
159+
expect(request?.url).toEqual("https://localhost:3000/api/v0/event");
160+
expect(request?.headers.get("Content-Type")).toEqual("application/json");
161+
expect(request?.headers.get("App-Key")).toEqual("A-DEV-000");
162+
});
163+
164+
it("should dispatch single event", async () => {
165+
fetchMock.mockResponseOnce("{}");
166+
167+
dispatcher.enqueue(createEvent("app_started"));
168+
169+
expectRequestCount(1);
170+
const body = await fetchMock.requests().at(0)?.json();
171+
expect(body.eventName).toEqual("app_started");
172+
});
173+
174+
it("should dispatch multiple events individually", async () => {
175+
fetchMock.mockResponseOnce("{}");
176+
fetchMock.mockResponseOnce("{}");
177+
178+
dispatcher.enqueue([createEvent("app_started"), createEvent("app_exited")]);
179+
180+
expectRequestCount(2);
181+
const body1 = await fetchMock.requests().at(0)?.json();
182+
const body2 = await fetchMock.requests().at(1)?.json();
183+
expect(body1.eventName).toEqual("app_started");
184+
expect(body2.eventName).toEqual("app_exited");
185+
});
186+
187+
it("should not retry requests that failed with 4xx", async () => {
188+
fetchMock.mockResponseOnce("{}", { status: 400 });
189+
190+
dispatcher.enqueue(createEvent("hello_world"));
191+
192+
expectRequestCount(1);
193+
const body = await fetchMock.requests().at(0)?.json();
194+
expect(body.eventName).toEqual("hello_world");
195+
196+
dispatcher.enqueue(createEvent("hello_world"));
197+
198+
expectRequestCount(2);
199+
});
200+
});

0 commit comments

Comments
 (0)