Skip to content

Commit b139522

Browse files
committed
feat(expo): system info collector assembling mobile device context
1 parent 13b44f1 commit b139522

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { test, expect, mock } from "bun:test"
2+
3+
// Mock the native modules BEFORE importing the collector.
4+
mock.module("react-native", () => ({
5+
Platform: { OS: "ios", Version: "17.4" },
6+
Dimensions: {
7+
get: (k: "window" | "screen") =>
8+
k === "window" ? { width: 390, height: 844 } : { width: 1179, height: 2556 },
9+
},
10+
PixelRatio: { get: () => 3 },
11+
}))
12+
mock.module("expo-device", () => ({
13+
modelName: "iPhone 15",
14+
}))
15+
mock.module("expo-constants", () => ({
16+
default: { expoConfig: { version: "1.2.3" }, nativeBuildVersion: "42" },
17+
}))
18+
mock.module("@react-native-community/netinfo", () => ({
19+
fetch: async () => ({
20+
isConnected: true,
21+
details: { effectiveType: "4g", rtt: 50, downlink: 10 },
22+
}),
23+
}))
24+
25+
test("assembles a mobile SystemInfo record", async () => {
26+
const { collectSystemInfo } = await import("./system-info")
27+
const info = await collectSystemInfo({ pageUrl: "myapp://home" })
28+
expect(info.devicePlatform).toBe("ios")
29+
expect(info.deviceModel).toBe("iPhone 15")
30+
expect(info.appVersion).toBe("1.2.3")
31+
expect(info.appBuild).toBe("42")
32+
expect(info.osVersion).toBe("17.4")
33+
expect(info.viewport).toEqual({ w: 390, h: 844 })
34+
expect(info.screen).toEqual({ w: 1179, h: 2556 })
35+
expect(info.dpr).toBe(3)
36+
expect(info.online).toBe(true)
37+
expect(info.connection?.effectiveType).toBe("4g")
38+
expect(info.pageUrl).toBe("myapp://home")
39+
expect(info.userAgent).toMatch(/Expo/)
40+
})
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Platform, Dimensions, PixelRatio } from "react-native"
2+
import * as Device from "expo-device"
3+
import Constants from "expo-constants"
4+
import { fetch as fetchNetInfo } from "@react-native-community/netinfo"
5+
import type { SystemInfo } from "@reprojs/shared"
6+
7+
export async function collectSystemInfo(opts: { pageUrl: string }): Promise<SystemInfo> {
8+
const os = Platform.OS
9+
const devicePlatform = os === "ios" ? "ios" : os === "android" ? "android" : undefined
10+
const windowDims = Dimensions.get("window")
11+
const screenDims = Dimensions.get("screen")
12+
const net = await fetchNetInfo().catch(() => null)
13+
const language =
14+
(typeof Intl !== "undefined" && Intl.DateTimeFormat().resolvedOptions().locale) || "en"
15+
const timezone =
16+
(typeof Intl !== "undefined" && Intl.DateTimeFormat().resolvedOptions().timeZone) || "UTC"
17+
const timezoneOffset = -new Date().getTimezoneOffset()
18+
const appVersion = (Constants.expoConfig?.version as string | undefined) ?? undefined
19+
const appBuild =
20+
((Constants as unknown as { nativeBuildVersion?: string }).nativeBuildVersion as
21+
| string
22+
| undefined) ?? undefined
23+
24+
return {
25+
userAgent: `Expo/${(Constants as unknown as { expoVersion?: string }).expoVersion ?? "unknown"} ${os} ${Platform.Version}`,
26+
platform: os,
27+
devicePlatform,
28+
appVersion,
29+
appBuild,
30+
deviceModel: Device.modelName ?? undefined,
31+
osVersion: String(Platform.Version),
32+
language,
33+
timezone,
34+
timezoneOffset,
35+
viewport: { w: Math.round(windowDims.width), h: Math.round(windowDims.height) },
36+
screen: { w: Math.round(screenDims.width), h: Math.round(screenDims.height) },
37+
dpr: PixelRatio.get(),
38+
online: !!net?.isConnected,
39+
connection: net?.details
40+
? {
41+
effectiveType: (net.details as { effectiveType?: string }).effectiveType ?? undefined,
42+
rtt: (net.details as { rtt?: number }).rtt,
43+
downlink: (net.details as { downlink?: number }).downlink,
44+
}
45+
: undefined,
46+
pageUrl: opts.pageUrl,
47+
timestamp: new Date().toISOString(),
48+
}
49+
}

0 commit comments

Comments
 (0)