feat(analytics): provider-pluggable AnalyticsDriver, strip direct provider deps

@luxfi/wallet-analytics mirrors the abstraction the parallel exchange Blue
landed at ~/work/lux/exchange/pkgs/utilities/src/telemetry/analytics/backend.ts.
No third-party SDK is imported by this module — delivery is owned by an
AnalyticsDriver registered via setAnalyticsDriver(...). Hanzo Insights is
the intended default driver; white-labels swap drivers at boot.

Public surface (init/track/identify/Identify/getUserId/setDeviceId/flush)
matches what wallet call sites already expect, so swapping providers does
not require diff-ing 200+ sendAnalyticsEvent(...) sites.

Stripped from package.json (zero direct provider deps now in our shipped
manifests):
  apps/extension: @datadog/browser-rum
  apps/mobile:    @amplitude/analytics-react-native
                  @datadog/mobile-react-native
                  @datadog/mobile-react-navigation
                  @datadog/datadog-ci

Both apps now declare workspace deps on @luxfi/wallet-analytics and
@luxfi/wallet-brand. The 6 source files that still call datadogRum / DdSdk
directly are upstream-shaped and broken on other axes (`@universe/*`,
`wallet/*` referenced as bare specifiers); they will be replaced with
@luxfi/wallet-analytics calls during the apps refactor.
This commit is contained in:
Hanzo AI
2026-04-30 11:47:26 -07:00
parent 67e571f2ed
commit bf5972ffe3
5 changed files with 180 additions and 5 deletions
+2 -1
View File
@@ -4,7 +4,6 @@
"browserslist": "last 2 chrome versions",
"dependencies": {
"@apollo/client": "3.11.10",
"@datadog/browser-rum": "5.23.3",
"@ethersproject/bignumber": "5.7.0",
"@ethersproject/providers": "5.7.2",
"@ethersproject/strings": "5.7.0",
@@ -51,6 +50,8 @@
"typed-redux-saga": "1.5.0",
"ua-parser-js": "1.0.37",
"@luxfi/wallet": "workspace:^",
"@luxfi/wallet-analytics": "workspace:^",
"@luxfi/wallet-brand": "workspace:^",
"wxt": "0.20.8",
"zod": "4.3.6",
"zustand": "5.0.6"
+2 -4
View File
@@ -71,10 +71,7 @@
"includedScripts": []
},
"dependencies": {
"@amplitude/analytics-react-native": "1.4.11",
"@apollo/client": "3.11.10",
"@datadog/mobile-react-native": "2.12.2",
"@datadog/mobile-react-navigation": "2.12.2",
"@ethersproject/bignumber": "5.7.0",
"@ethersproject/shims": "5.6.0",
"@expo/fingerprint": "0.15.3",
@@ -197,6 +194,8 @@
"@l.x/utils": "^1.1.3",
"uuid": "9.0.0",
"@luxfi/wallet": "workspace:^",
"@luxfi/wallet-analytics": "workspace:^",
"@luxfi/wallet-brand": "workspace:^",
"zod": "4.3.6",
"zustand": "5.0.6"
},
@@ -206,7 +205,6 @@
"@babel/plugin-proposal-logical-assignment-operators": "7.16.7",
"@babel/plugin-proposal-numeric-separator": "7.16.7",
"@babel/runtime": "7.26.0",
"@datadog/datadog-ci": "2.48.0",
"@react-native-community/cli": "18.0.1",
"@react-native-community/cli-platform-android": "18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0",
+13
View File
@@ -0,0 +1,13 @@
{
"name": "@luxfi/wallet-analytics",
"version": "0.1.0",
"private": false,
"license": "GPL-3.0-or-later",
"main": "src/index.ts",
"types": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./package.json": "./package.json"
},
"files": ["src"]
}
+152
View File
@@ -0,0 +1,152 @@
/**
* Provider-pluggable analytics for the Lux Wallet.
*
* Pattern matches `~/work/lux/exchange/pkgs/utilities/src/telemetry/analytics/backend.ts`.
* No third-party SDK is loaded by this module. Delivery is owned by an
* `AnalyticsDriver` registered via `setAnalyticsDriver(...)`.
*
* Default wiring (when wallet apps are integrated):
* - Web/extension → Hanzo Insights (PostHog wire protocol; configurable host).
* - Mobile → Hanzo Insights via the same driver shape.
* - White-labels override via `setAnalyticsDriver(...)` at app boot.
*
* Public surface (`init`/`track`/`identify`/`Identify`/`getUserId`/...) matches
* the existing wallet call sites so swapping providers requires no diff to
* `sendAnalyticsEvent(...)` consumers.
*/
export interface AnalyticsDriver {
/** Send an event with optional properties. */
capture(eventName: string, properties?: Record<string, unknown>): void
/** Associate the current session with a user id and optional traits. */
identify?(userId: string, properties?: Record<string, unknown>): void
/** Merge user-level properties (`Identify().set(...)`). */
setUserProperties?(properties: Record<string, unknown>): void
/** Hint the driver to flush queued events. */
flush?(): void | Promise<void>
/** Reset session — clears user id, device id, person properties. */
reset?(): void
}
let driver: AnalyticsDriver | null = null
let userIdCache: string | undefined
let deviceIdCache: string | undefined
const userProps: Record<string, unknown> = {}
export function setAnalyticsDriver(d: AnalyticsDriver | null): void {
driver = d
}
export function getAnalyticsDriver(): AnalyticsDriver | null {
return driver
}
export type InitOptions = {
transportProvider?: unknown
trackingOptions?: unknown
[key: string]: unknown
}
export function init(_apiKey: string, initialUserId?: string, _opts?: InitOptions): void {
if (initialUserId) {
userIdCache = initialUserId
}
}
export function track(eventName: string, properties?: Record<string, unknown>): void {
driver?.capture(eventName, properties)
}
export function flush(): void {
void driver?.flush?.()
}
export function getUserId(): string | undefined {
return userIdCache
}
export function setUserId(id: string | undefined): void {
userIdCache = id
if (id) {
driver?.identify?.(id, { ...userProps })
}
}
export function setDeviceId(id: string): void {
deviceIdCache = id
driver?.setUserProperties?.({ device_id: id })
}
export function getDeviceId(): string | undefined {
return deviceIdCache
}
export function reset(): void {
userIdCache = undefined
deviceIdCache = undefined
Object.keys(userProps).forEach((k) => {
delete userProps[k]
})
driver?.reset?.()
}
/**
* Operations builder for user-property merges. Matches the existing
* `Identify` API the wallet uses against legacy providers — call sites
* don't change when the driver is swapped.
*/
export class Identify {
private ops: Array<() => void> = []
set(property: string, value: unknown): this {
this.ops.push(() => {
userProps[property] = value
})
return this
}
setOnce(property: string, value: unknown): this {
this.ops.push(() => {
if (!(property in userProps)) {
userProps[property] = value
}
})
return this
}
postInsert(property: string, value: unknown): this {
this.ops.push(() => {
const current = userProps[property]
userProps[property] = Array.isArray(current) ? [...current, value] : [value]
})
return this
}
unset(property: string): this {
this.ops.push(() => {
delete userProps[property]
})
return this
}
clearAll(): this {
this.ops.push(() => {
Object.keys(userProps).forEach((k) => {
delete userProps[k]
})
})
return this
}
/** @internal — applied by `identify(builder)`. */
apply(): Record<string, unknown> {
this.ops.forEach((op) => op())
this.ops = []
return userProps
}
}
export function identify(i: Identify): void {
const merged = i.apply()
driver?.setUserProperties?.({ ...merged })
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
"moduleResolution": "Bundler",
"lib": ["dom", "esnext"],
"noEmit": true,
"types": []
}
}