onedollarstats 0.0.24 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/dist/expo.js +76 -15
- package/dist/expo.js.map +2 -2
- package/dist/index.js +4 -1
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ event("Purchase", "/product", { amount: 1, color: "green" });
|
|
|
121
121
|
|
|
122
122
|
## Expo
|
|
123
123
|
|
|
124
|
-
`onedollarstats/expo` is a dedicated entry point for Expo apps using `expo-router`. It auto-collects pageviews on route change and
|
|
124
|
+
`onedollarstats/expo` is a dedicated entry point for Expo apps using `expo-router`. It auto-collects pageviews on route change and when the app returns from the background (not for system overlays like Face ID or permission prompts), supports dynamic-route templates (`/profile/[id]` instead of `/profile/abc123`), and sends events natively on iOS/Android and via image beacon + `sendBeacon` on web. Native requests carry a `User-Agent` such as `Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X; Apple) onedollarstats-expo`, so the dashboard shows the OS (iOS or Android) and the device type (phone or tablet). React Native's default UA (`okhttp` or `CFNetwork`) would show Android traffic as unknown and every iPad as a phone.
|
|
125
125
|
|
|
126
126
|
```tsx
|
|
127
127
|
// app/_layout.tsx
|
|
@@ -150,13 +150,25 @@ view({ campaign: 'spring' }); // pageview with just props
|
|
|
150
150
|
view('/landing', { campaign: 'spring' }); // pageview with explicit path
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
As on the web, a `view()` of the page that was just counted is skipped, whether autocollect or an earlier `view()` counted it. Paths are compared by the URL they produce, so `'/landing'`, `'landing/'` and `'/landing?ref=ad'` are the same page. That means `view({ campaign: 'spring' })` on a screen autocollect already counted sends nothing. If you want to send props with each screen's pageview, set `autocollect: false` and call `view()` yourself. `event()` is never deduplicated.
|
|
154
|
+
|
|
153
155
|
**Expo-specific config options:**
|
|
154
156
|
|
|
155
157
|
| Option | Type | Default | Description |
|
|
156
158
|
| ----------------------- | ---------- | ------- | -------------------------------------------------------------------------------------------- |
|
|
157
159
|
| `collapseDynamicRoutes` | `boolean` | `true` | Use `useSegments()` to record routes as templates (`/profile/[id]`) instead of concrete paths. Group segments like `(tabs)` are stripped. |
|
|
158
160
|
|
|
159
|
-
|
|
161
|
+
`hostname` is required. `collectorUrl` and `autocollect` work as in the web tracker above. `devmode` is covered under **Development builds** below. `trackLocalhostAs` and `hashRouting` aren't supported.
|
|
162
|
+
|
|
163
|
+
**`excludePages` / `includePages` work differently from the web tracker:**
|
|
164
|
+
|
|
165
|
+
- Entries match a path and everything under it, with no wildcards. `'/admin'` matches `/admin` and `/admin/users`, but not `/administrator`.
|
|
166
|
+
- When `includePages` is set, `excludePages` is ignored. The web tracker applies both.
|
|
167
|
+
- `includePages: ['/']` matches every screen, because every path is under `/`.
|
|
168
|
+
- With `collapseDynamicRoutes` on (the default), match against the template, for example `'/profile/[id]'`, not `'/profile/abc123'`.
|
|
169
|
+
- As on the web, manual `view()` and `event()` calls ignore both lists.
|
|
170
|
+
|
|
171
|
+
**Development builds:** events are ignored in development, like `localhost` on the web. That covers dev bundles (`__DEV__`: Expo Go, dev clients, `npx expo start`) on iOS, Android and web, plus Expo web served from `localhost`. Set `devmode: true` to send them as debug events. In release builds and on your real domain, `devmode` is ignored, so leaving it on can't turn production traffic into debug events.
|
|
160
172
|
|
|
161
173
|
## Autocapture
|
|
162
174
|
|
package/dist/expo.js
CHANGED
|
@@ -15,7 +15,8 @@ function mergeConfig(config) {
|
|
|
15
15
|
hostname: config.hostname,
|
|
16
16
|
collectorUrl: config.collectorUrl ?? "https://collector.onedollarstats.com/events",
|
|
17
17
|
autocollect: config.autocollect ?? true,
|
|
18
|
-
|
|
18
|
+
// Only honoured in development, so a release build can't send debug events
|
|
19
|
+
devmode: isDevEnvironment() ? config.devmode ?? false : false,
|
|
19
20
|
collapseDynamicRoutes: config.collapseDynamicRoutes ?? true,
|
|
20
21
|
excludePages: config.excludePages,
|
|
21
22
|
includePages: config.includePages
|
|
@@ -38,7 +39,13 @@ function isWebLocalhost() {
|
|
|
38
39
|
if (Platform.OS !== "web") return false;
|
|
39
40
|
if (typeof window === "undefined" || !window.location) return false;
|
|
40
41
|
const { hostname, protocol } = window.location;
|
|
41
|
-
return
|
|
42
|
+
return (
|
|
43
|
+
// localhost, 127.x.x.x, [::1]
|
|
44
|
+
/^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(hostname) && (protocol === "http:" || protocol === "https:")
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
function isDevEnvironment() {
|
|
48
|
+
return typeof __DEV__ !== "undefined" && __DEV__ === true || isWebLocalhost();
|
|
42
49
|
}
|
|
43
50
|
function useRequiredContext(caller) {
|
|
44
51
|
const ctx = useContext(Context);
|
|
@@ -63,31 +70,45 @@ function OneDollarStatsProvider({ config, children }) {
|
|
|
63
70
|
]
|
|
64
71
|
);
|
|
65
72
|
const lastPathRef = useRef(null);
|
|
73
|
+
const currentPathRef = useRef(null);
|
|
66
74
|
const announcedRef = useRef(false);
|
|
67
75
|
const trackedPath = useTrackedPath(merged);
|
|
68
76
|
useEffect(() => {
|
|
69
77
|
if (announcedRef.current) return;
|
|
70
|
-
if (
|
|
78
|
+
if (isDevEnvironment()) {
|
|
71
79
|
console.log(
|
|
72
|
-
`[onedollarstats]
|
|
73
|
-
OneDollarStats connected! Tracking localhost as ${merged.hostname}`
|
|
80
|
+
merged.devmode ? `[onedollarstats]
|
|
81
|
+
OneDollarStats connected! Tracking ${isWebLocalhost() ? "localhost" : "this dev build"} as ${merged.hostname}` : `[onedollarstats]
|
|
82
|
+
Development detected: events are not sent. Set devmode: true to test tracking.`
|
|
74
83
|
);
|
|
75
84
|
}
|
|
76
85
|
announcedRef.current = true;
|
|
77
86
|
}, [merged.devmode, merged.hostname]);
|
|
78
87
|
useEffect(() => {
|
|
88
|
+
currentPathRef.current = trackedPath;
|
|
79
89
|
if (!merged.autocollect) return;
|
|
80
|
-
if (isExcluded(trackedPath, merged))
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
if (isExcluded(trackedPath, merged)) {
|
|
91
|
+
lastPathRef.current = null;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const page = normalizePath(trackedPath);
|
|
95
|
+
if (lastPathRef.current === page) return;
|
|
96
|
+
lastPathRef.current = page;
|
|
83
97
|
send("PageView", trackedPath, merged);
|
|
84
98
|
}, [trackedPath, merged]);
|
|
85
99
|
useEffect(() => {
|
|
100
|
+
let wasInBackground = AppState.currentState === "background";
|
|
86
101
|
const handler = (state) => {
|
|
87
|
-
if (state
|
|
102
|
+
if (state === "background") {
|
|
103
|
+
wasInBackground = true;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (state !== "active" || !wasInBackground) return;
|
|
107
|
+
wasInBackground = false;
|
|
88
108
|
if (!merged.autocollect) return;
|
|
89
|
-
const current =
|
|
109
|
+
const current = currentPathRef.current;
|
|
90
110
|
if (!current || isExcluded(current, merged)) return;
|
|
111
|
+
lastPathRef.current = normalizePath(current);
|
|
91
112
|
send("PageView", current, merged);
|
|
92
113
|
};
|
|
93
114
|
const sub = AppState.addEventListener("change", handler);
|
|
@@ -114,6 +135,9 @@ function useAnalytics() {
|
|
|
114
135
|
(pathOrProps, props) => {
|
|
115
136
|
const targetPath = typeof pathOrProps === "string" ? pathOrProps : trackedPath;
|
|
116
137
|
const viewProps = typeof pathOrProps === "object" ? pathOrProps : props;
|
|
138
|
+
const page = normalizePath(targetPath);
|
|
139
|
+
if (ctx.lastPathRef.current === page) return;
|
|
140
|
+
ctx.lastPathRef.current = page;
|
|
117
141
|
send("PageView", targetPath, ctx.config, viewProps);
|
|
118
142
|
},
|
|
119
143
|
[ctx, trackedPath]
|
|
@@ -121,9 +145,7 @@ function useAnalytics() {
|
|
|
121
145
|
return { event, view };
|
|
122
146
|
}
|
|
123
147
|
function shouldSkipSend(config) {
|
|
124
|
-
|
|
125
|
-
if (isWebLocalhost() && !config.devmode) return true;
|
|
126
|
-
return false;
|
|
148
|
+
return isDevEnvironment() && !config.devmode;
|
|
127
149
|
}
|
|
128
150
|
function devLog(label, url, props) {
|
|
129
151
|
let msg = `[onedollarstats]
|
|
@@ -135,10 +157,26 @@ Props: ${JSON.stringify(props, null, 2)}`;
|
|
|
135
157
|
}
|
|
136
158
|
console.log(msg);
|
|
137
159
|
}
|
|
160
|
+
function buildUrl(hostname, path) {
|
|
161
|
+
return `https://${hostname}${normalizePath(path)}`;
|
|
162
|
+
}
|
|
163
|
+
function normalizePath(path) {
|
|
164
|
+
const bare = path.split(/[?#]/)[0];
|
|
165
|
+
const withSlash = bare.startsWith("/") ? bare : `/${bare}`;
|
|
166
|
+
const encoded = withSlash.replace(/[^\x21-\x7E]|["<>`{}]/gu, encodeChar);
|
|
167
|
+
return encoded.replace(/\/$/, "");
|
|
168
|
+
}
|
|
169
|
+
function encodeChar(char) {
|
|
170
|
+
try {
|
|
171
|
+
return encodeURIComponent(char);
|
|
172
|
+
} catch {
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
138
176
|
const SAFE_GET_THRESHOLD = 1500;
|
|
139
177
|
function send(eventName, path, config, props) {
|
|
140
178
|
if (shouldSkipSend(config)) return;
|
|
141
|
-
const url =
|
|
179
|
+
const url = buildUrl(config.hostname, path);
|
|
142
180
|
if (config.devmode) devLog(eventName, url, props);
|
|
143
181
|
const body = JSON.stringify({
|
|
144
182
|
u: url,
|
|
@@ -152,13 +190,36 @@ function send(eventName, path, config, props) {
|
|
|
152
190
|
}
|
|
153
191
|
}
|
|
154
192
|
function sendNative(collectorUrl, body) {
|
|
193
|
+
const userAgent = nativeUserAgent();
|
|
155
194
|
fetch(collectorUrl, {
|
|
156
195
|
method: "POST",
|
|
157
|
-
headers: { "Content-Type": "application/json" },
|
|
196
|
+
headers: { "Content-Type": "application/json", ...userAgent && { "User-Agent": userAgent } },
|
|
158
197
|
body
|
|
159
198
|
}).catch(() => {
|
|
160
199
|
});
|
|
161
200
|
}
|
|
201
|
+
function nativeUserAgent() {
|
|
202
|
+
try {
|
|
203
|
+
if (Platform.OS === "ios") {
|
|
204
|
+
const version = uaToken(Platform.Version).replace(/\./g, "_");
|
|
205
|
+
const device = Platform.isPad ? "iPad" : "iPhone";
|
|
206
|
+
const os = version ? `; CPU ${Platform.isPad ? "" : "iPhone "}OS ${version} like Mac OS X` : "";
|
|
207
|
+
return `Mozilla/5.0 (${device}${os}; Apple) ${UA_MARKER}`;
|
|
208
|
+
}
|
|
209
|
+
if (Platform.OS === "android") {
|
|
210
|
+
const { Release, Model } = Platform.constants;
|
|
211
|
+
const release = uaToken(Release);
|
|
212
|
+
const details = ["Linux", release ? `Android ${release}` : "Android", uaToken(Model)].filter(Boolean);
|
|
213
|
+
return `Mozilla/5.0 (${details.join("; ")}) ${UA_MARKER}`;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
const UA_MARKER = "onedollarstats-expo";
|
|
220
|
+
function uaToken(value) {
|
|
221
|
+
return String(value ?? "").replace(/[^\w .-]/g, "").trim().slice(0, 64);
|
|
222
|
+
}
|
|
162
223
|
function sendWeb(collectorUrl, body) {
|
|
163
224
|
const bytes = new TextEncoder().encode(body);
|
|
164
225
|
const bin = String.fromCharCode(...bytes);
|
package/dist/expo.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/expo.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n createContext,\n createElement,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n type MutableRefObject,\n type ReactNode\n} from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport { usePathname, useSegments } from 'expo-router';\n\nexport type ExpoAnalyticsConfig = {\n hostname: string;\n collectorUrl?: string;\n excludePages?: string[];\n includePages?: string[];\n autocollect?: boolean;\n devmode?: boolean;\n collapseDynamicRoutes?: boolean;\n};\n\ntype InternalConfig = {\n hostname: string;\n collectorUrl: string;\n autocollect: boolean;\n devmode: boolean;\n collapseDynamicRoutes: boolean;\n excludePages?: string[];\n includePages?: string[];\n};\n\n// TODO(page-scope): restore when page-scope detection is designed.\n/*\ntype OverrideSource = 'hook' | 'component';\ntype Override = { realPath: string; customPath: string; source: OverrideSource } | null;\ntype PropsOverride =\n | { realPath: string; props: Record<string, string>; source: OverrideSource }\n | null;\n*/\n\ntype ContextValue = {\n config: InternalConfig;\n lastPathRef: MutableRefObject<string | null>;\n // TODO(page-scope): restore when page-scope detection is designed.\n // overrideRef: MutableRefObject<Override>;\n // propsOverrideRef: MutableRefObject<PropsOverride>;\n};\n\nconst Context = createContext<ContextValue | null>(null);\n\n// TODO(page-scope): restore when page-scope detection is designed.\n/*\nfunction resolvePath(pathname: string, overrideRef: MutableRefObject<Override>): string {\n const o = overrideRef.current;\n return o && o.realPath === pathname ? o.customPath : pathname;\n}\n\nfunction resolveProps(\n pathname: string,\n propsOverrideRef: MutableRefObject<PropsOverride>\n): Record<string, string> | undefined {\n const o = propsOverrideRef.current;\n return o && o.realPath === pathname ? o.props : undefined;\n}\n\nfunction mergeProps(\n screenProps: Record<string, string> | undefined,\n explicitProps: Record<string, string> | undefined\n): Record<string, string> | undefined {\n if (!screenProps && !explicitProps) return undefined;\n if (!screenProps) return explicitProps;\n if (!explicitProps) return screenProps;\n return { ...screenProps, ...explicitProps };\n}\n*/\n\nfunction mergeConfig(config: ExpoAnalyticsConfig): InternalConfig {\n return {\n hostname: config.hostname,\n collectorUrl: config.collectorUrl ?? 'https://collector.onedollarstats.com/events',\n autocollect: config.autocollect ?? true,\n devmode: config.devmode ?? false,\n collapseDynamicRoutes: config.collapseDynamicRoutes ?? true,\n excludePages: config.excludePages,\n includePages: config.includePages\n };\n}\n\nfunction isGroupSegment(segment: string): boolean {\n return /^\\(.+\\)$/.test(segment);\n}\n\nfunction collapsePath(segments: readonly string[]): string {\n const visible = segments.filter(s => !isGroupSegment(s));\n if (visible.length === 0) return '/';\n return '/' + visible.join('/');\n}\n\nfunction useTrackedPath(config: InternalConfig): string {\n const pathname = usePathname();\n const segments = useSegments();\n return config.collapseDynamicRoutes ? collapsePath(segments as readonly string[]) : pathname;\n}\n\nfunction isWebLocalhost(): boolean {\n if (Platform.OS !== 'web') return false;\n if (typeof window === 'undefined' || !window.location) return false;\n const { hostname, protocol } = window.location;\n return (\n /^localhost$|^127(\\.[0-9]+){0,2}\\.[0-9]+$|^\\[::1?\\]$/.test(hostname) &&\n (protocol === 'http:' || protocol === 'https:')\n );\n}\n\nfunction useRequiredContext(caller: string): ContextValue {\n const ctx = useContext(Context);\n if (!ctx) {\n throw new Error(\n `[onedollarstats] ${caller} must be used inside <OneDollarStatsProvider>. ` +\n `Wrap your root layout with the provider.`\n );\n }\n return ctx;\n}\n\nexport type OneDollarStatsProviderProps = {\n config: ExpoAnalyticsConfig;\n children: ReactNode;\n};\n\nexport function OneDollarStatsProvider({ config, children }: OneDollarStatsProviderProps) {\n const merged = useMemo(\n () => mergeConfig(config),\n [\n config.hostname,\n config.collectorUrl,\n config.autocollect,\n config.devmode,\n config.collapseDynamicRoutes,\n config.excludePages,\n config.includePages\n ]\n );\n\n const lastPathRef = useRef<string | null>(null);\n // TODO(page-scope): restore when page-scope detection is designed.\n // const overrideRef = useRef<Override>(null);\n // const propsOverrideRef = useRef<PropsOverride>(null);\n const announcedRef = useRef(false);\n const trackedPath = useTrackedPath(merged);\n\n useEffect(() => {\n if (announcedRef.current) return;\n if (merged.devmode && isWebLocalhost()) {\n console.log(\n `[onedollarstats]\\nOneDollarStats connected! Tracking localhost as ${merged.hostname}`\n );\n }\n announcedRef.current = true;\n }, [merged.devmode, merged.hostname]);\n\n useEffect(() => {\n if (!merged.autocollect) return;\n if (isExcluded(trackedPath, merged)) return;\n if (lastPathRef.current === trackedPath) return;\n lastPathRef.current = trackedPath;\n send('PageView', trackedPath, merged);\n }, [trackedPath, merged]);\n\n useEffect(() => {\n const handler = (state: AppStateStatus) => {\n if (state !== 'active') return;\n if (!merged.autocollect) return;\n const current = lastPathRef.current;\n if (!current || isExcluded(current, merged)) return;\n send('PageView', current, merged);\n };\n const sub = AppState.addEventListener('change', handler);\n return () => sub.remove();\n }, [merged]);\n\n const value = useMemo<ContextValue>(\n () => ({ config: merged, lastPathRef }),\n [merged]\n );\n\n return createElement(Context.Provider, { value }, children);\n}\n\ntype Props = Record<string, string>;\n\nexport type AnalyticsAPI = {\n event(eventName: string, pathOrProps?: string | Props, props?: Props): void;\n view(pathOrProps?: string | Props, props?: Props): void;\n};\n\nexport function useAnalytics(): AnalyticsAPI {\n const ctx = useRequiredContext('useAnalytics');\n const trackedPath = useTrackedPath(ctx.config);\n\n const event = useCallback(\n (eventName: string, pathOrProps?: string | Props, props?: Props) => {\n const targetPath = typeof pathOrProps === 'string' ? pathOrProps : trackedPath;\n const eventProps = typeof pathOrProps === 'object' ? pathOrProps : props;\n send(eventName, targetPath, ctx.config, eventProps);\n },\n [ctx, trackedPath]\n );\n\n const view = useCallback(\n (pathOrProps?: string | Props, props?: Props) => {\n const targetPath = typeof pathOrProps === 'string' ? pathOrProps : trackedPath;\n const viewProps = typeof pathOrProps === 'object' ? pathOrProps : props;\n send('PageView', targetPath, ctx.config, viewProps);\n },\n [ctx, trackedPath]\n );\n\n return { event, view };\n}\n\n// TODO(page-scope): restore when page-scope detection is designed.\n/*\nexport function useAnalyticsPath(customPath: string): void {\n const ctx = useRequiredContext('useAnalyticsPath');\n const pathname = usePathname();\n useEffect(() => {\n const entry: Override = { realPath: pathname, customPath, source: 'hook' };\n ctx.overrideRef.current = entry;\n return () => {\n if (ctx.overrideRef.current === entry) ctx.overrideRef.current = null;\n };\n }, [ctx, pathname, customPath]);\n}\n\nexport type AnalyticsPathProps = { path: string };\n\nexport function AnalyticsPath({ path }: AnalyticsPathProps): null {\n const ctx = useRequiredContext('AnalyticsPath');\n const pathname = usePathname();\n useEffect(() => {\n const existing = ctx.overrideRef.current;\n if (existing && existing.realPath === pathname && existing.source === 'hook') return;\n const entry: Override = { realPath: pathname, customPath: path, source: 'component' };\n ctx.overrideRef.current = entry;\n return () => {\n if (ctx.overrideRef.current === entry) ctx.overrideRef.current = null;\n };\n }, [ctx, pathname, path]);\n return null;\n}\n\nexport function useAnalyticsProps(props: Record<string, string>): void {\n const ctx = useRequiredContext('useAnalyticsProps');\n const pathname = usePathname();\n useEffect(() => {\n const entry: PropsOverride = { realPath: pathname, props, source: 'hook' };\n ctx.propsOverrideRef.current = entry;\n return () => {\n if (ctx.propsOverrideRef.current === entry) ctx.propsOverrideRef.current = null;\n };\n }, [ctx, pathname, props]);\n}\n\nexport type AnalyticsPropsProps = Record<string, string>;\n\nexport function AnalyticsProps(props: AnalyticsPropsProps): null {\n const ctx = useRequiredContext('AnalyticsProps');\n const pathname = usePathname();\n useEffect(() => {\n const existing = ctx.propsOverrideRef.current;\n if (existing && existing.realPath === pathname && existing.source === 'hook') return;\n const entry: PropsOverride = { realPath: pathname, props, source: 'component' };\n ctx.propsOverrideRef.current = entry;\n return () => {\n if (ctx.propsOverrideRef.current === entry) ctx.propsOverrideRef.current = null;\n };\n }, [ctx, pathname, props]);\n return null;\n}\n*/\n\nfunction shouldSkipSend(config: InternalConfig): boolean {\n if (Platform.OS !== 'web') return false;\n if (isWebLocalhost() && !config.devmode) return true;\n return false;\n}\n\nfunction devLog(label: string, url: string, props?: Record<string, string>): void {\n let msg = `[onedollarstats]\\nEvent name: ${label}\\nEvent collected from: ${url}`;\n if (props && Object.keys(props).length > 0) {\n msg += `\\nProps: ${JSON.stringify(props, null, 2)}`;\n }\n console.log(msg);\n}\n\nconst SAFE_GET_THRESHOLD = 1500;\n\nfunction send(\n eventName: string,\n path: string,\n config: InternalConfig,\n props?: Record<string, string>\n): void {\n if (shouldSkipSend(config)) return;\n const url = `https://${config.hostname}${path}`;\n if (config.devmode) devLog(eventName, url, props);\n\n const body = JSON.stringify({\n u: url,\n e: [{ t: eventName, ...(props && { p: props }) }],\n debug: config.devmode\n });\n\n if (Platform.OS === 'web') {\n sendWeb(config.collectorUrl, body);\n } else {\n sendNative(config.collectorUrl, body);\n }\n}\n\nfunction sendNative(collectorUrl: string, body: string): void {\n fetch(collectorUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body\n }).catch(() => {});\n}\n\nfunction sendWeb(collectorUrl: string, body: string): void {\n const bytes = new TextEncoder().encode(body);\n const bin = String.fromCharCode(...bytes);\n const payloadBase64 = btoa(bin);\n\n if (payloadBase64.length <= SAFE_GET_THRESHOLD) {\n const img = new Image(1, 1);\n img.onerror = () => sendBeaconOrFetch(collectorUrl, body);\n img.src = `${collectorUrl}?data=${payloadBase64}`;\n return;\n }\n\n sendBeaconOrFetch(collectorUrl, body);\n}\n\nfunction sendBeaconOrFetch(collectorUrl: string, body: string): void {\n if (typeof navigator !== 'undefined' && navigator.sendBeacon?.(collectorUrl, body)) {\n return;\n }\n\n fetch(collectorUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n keepalive: true\n }).catch(() => {});\n}\n\nfunction pathMatches(path: string, prefix: string): boolean {\n return path === prefix || path.startsWith(prefix.endsWith('/') ? prefix : prefix + '/');\n}\n\nfunction isExcluded(path: string, config: InternalConfig): boolean {\n if (config.includePages?.length) {\n return !config.includePages.some(p => pathMatches(path, p));\n }\n if (config.excludePages?.length) {\n return config.excludePages.some(p => pathMatches(path, p));\n }\n return false;\n}\n"],
|
|
5
|
-
"mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,UAAU,gBAAqC;AACxD,SAAS,aAAa,mBAAmB;
|
|
4
|
+
"sourcesContent": ["import {\n createContext,\n createElement,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n type MutableRefObject,\n type ReactNode\n} from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport { usePathname, useSegments } from 'expo-router';\n\nexport type ExpoAnalyticsConfig = {\n hostname: string;\n collectorUrl?: string;\n excludePages?: string[];\n includePages?: string[];\n autocollect?: boolean;\n devmode?: boolean;\n collapseDynamicRoutes?: boolean;\n};\n\ntype InternalConfig = {\n hostname: string;\n collectorUrl: string;\n autocollect: boolean;\n devmode: boolean;\n collapseDynamicRoutes: boolean;\n excludePages?: string[];\n includePages?: string[];\n};\n\n// TODO(page-scope): restore when page-scope detection is designed.\n/*\ntype OverrideSource = 'hook' | 'component';\ntype Override = { realPath: string; customPath: string; source: OverrideSource } | null;\ntype PropsOverride =\n | { realPath: string; props: Record<string, string>; source: OverrideSource }\n | null;\n*/\n\ntype ContextValue = {\n config: InternalConfig;\n // Last pageview sent (normalised path), shared by autocollect and manual view() for dedup\n lastPathRef: MutableRefObject<string | null>;\n // TODO(page-scope): restore when page-scope detection is designed.\n // overrideRef: MutableRefObject<Override>;\n // propsOverrideRef: MutableRefObject<PropsOverride>;\n};\n\nconst Context = createContext<ContextValue | null>(null);\n\n// TODO(page-scope): restore when page-scope detection is designed.\n/*\nfunction resolvePath(pathname: string, overrideRef: MutableRefObject<Override>): string {\n const o = overrideRef.current;\n return o && o.realPath === pathname ? o.customPath : pathname;\n}\n\nfunction resolveProps(\n pathname: string,\n propsOverrideRef: MutableRefObject<PropsOverride>\n): Record<string, string> | undefined {\n const o = propsOverrideRef.current;\n return o && o.realPath === pathname ? o.props : undefined;\n}\n\nfunction mergeProps(\n screenProps: Record<string, string> | undefined,\n explicitProps: Record<string, string> | undefined\n): Record<string, string> | undefined {\n if (!screenProps && !explicitProps) return undefined;\n if (!screenProps) return explicitProps;\n if (!explicitProps) return screenProps;\n return { ...screenProps, ...explicitProps };\n}\n*/\n\nfunction mergeConfig(config: ExpoAnalyticsConfig): InternalConfig {\n return {\n hostname: config.hostname,\n collectorUrl: config.collectorUrl ?? 'https://collector.onedollarstats.com/events',\n autocollect: config.autocollect ?? true,\n // Only honoured in development, so a release build can't send debug events\n devmode: isDevEnvironment() ? (config.devmode ?? false) : false,\n collapseDynamicRoutes: config.collapseDynamicRoutes ?? true,\n excludePages: config.excludePages,\n includePages: config.includePages\n };\n}\n\n// Groups like (tabs) aren't part of the URL\nfunction isGroupSegment(segment: string): boolean {\n return /^\\(.+\\)$/.test(segment);\n}\n\n// Segments keep the file names, so /profile/abc becomes /profile/[id]\nfunction collapsePath(segments: readonly string[]): string {\n const visible = segments.filter(s => !isGroupSegment(s));\n if (visible.length === 0) return '/';\n return '/' + visible.join('/');\n}\n\nfunction useTrackedPath(config: InternalConfig): string {\n const pathname = usePathname();\n const segments = useSegments();\n return config.collapseDynamicRoutes ? collapsePath(segments as readonly string[]) : pathname;\n}\n\nfunction isWebLocalhost(): boolean {\n if (Platform.OS !== 'web') return false;\n if (typeof window === 'undefined' || !window.location) return false;\n const { hostname, protocol } = window.location;\n return (\n // localhost, 127.x.x.x, [::1]\n /^localhost$|^127(\\.[0-9]+){0,2}\\.[0-9]+$|^\\[::1?\\]$/.test(hostname) &&\n (protocol === 'http:' || protocol === 'https:')\n );\n}\n\n// Dev bundle (Metro sets __DEV__, native and web) or Expo web on localhost\nfunction isDevEnvironment(): boolean {\n return (typeof __DEV__ !== 'undefined' && __DEV__ === true) || isWebLocalhost();\n}\n\nfunction useRequiredContext(caller: string): ContextValue {\n const ctx = useContext(Context);\n if (!ctx) {\n throw new Error(\n `[onedollarstats] ${caller} must be used inside <OneDollarStatsProvider>. ` +\n `Wrap your root layout with the provider.`\n );\n }\n return ctx;\n}\n\nexport type OneDollarStatsProviderProps = {\n config: ExpoAnalyticsConfig;\n children: ReactNode;\n};\n\nexport function OneDollarStatsProvider({ config, children }: OneDollarStatsProviderProps) {\n // Fields, not `config`: an inline config object is new on every render\n const merged = useMemo(\n () => mergeConfig(config),\n [\n config.hostname,\n config.collectorUrl,\n config.autocollect,\n config.devmode,\n config.collapseDynamicRoutes,\n config.excludePages,\n config.includePages\n ]\n );\n\n const lastPathRef = useRef<string | null>(null);\n // For the AppState handler, which only re-subscribes on config changes\n const currentPathRef = useRef<string | null>(null);\n // TODO(page-scope): restore when page-scope detection is designed.\n // const overrideRef = useRef<Override>(null);\n // const propsOverrideRef = useRef<PropsOverride>(null);\n const announcedRef = useRef(false);\n const trackedPath = useTrackedPath(merged);\n\n // Once per mount, not on every config change\n useEffect(() => {\n if (announcedRef.current) return;\n if (isDevEnvironment()) {\n console.log(\n merged.devmode\n ? `[onedollarstats]\\nOneDollarStats connected! Tracking ${isWebLocalhost() ? 'localhost' : 'this dev build'} as ${merged.hostname}`\n : `[onedollarstats]\\nDevelopment detected: events are not sent. Set devmode: true to test tracking.`\n );\n }\n announcedRef.current = true;\n }, [merged.devmode, merged.hostname]);\n\n useEffect(() => {\n currentPathRef.current = trackedPath;\n if (!merged.autocollect) return;\n if (isExcluded(trackedPath, merged)) {\n // Forget the last page, or A \u2192 excluded \u2192 A would look like a duplicate\n lastPathRef.current = null;\n return;\n }\n // Re-renders, /profile/a \u2192 /profile/b (same template) and a manual view() of this screen aren't new views\n const page = normalizePath(trackedPath);\n if (lastPathRef.current === page) return;\n lastPathRef.current = page;\n send('PageView', trackedPath, merged);\n }, [trackedPath, merged]);\n\n useEffect(() => {\n // Count only returns from the background. iOS also flips to inactive and back\n // for Face ID, permission prompts and Control Center.\n // currentState, not 'active': this re-subscribes on config changes, maybe while backgrounded.\n let wasInBackground = AppState.currentState === 'background';\n const handler = (state: AppStateStatus) => {\n if (state === 'background') {\n wasInBackground = true;\n return;\n }\n if (state !== 'active' || !wasInBackground) return;\n wasInBackground = false;\n if (!merged.autocollect) return;\n // The screen the user is on now, not the last tracked one (that's stale on excluded screens)\n const current = currentPathRef.current;\n if (!current || isExcluded(current, merged)) return;\n lastPathRef.current = normalizePath(current);\n send('PageView', current, merged);\n };\n const sub = AppState.addEventListener('change', handler);\n return () => sub.remove();\n }, [merged]);\n\n const value = useMemo<ContextValue>(\n () => ({ config: merged, lastPathRef }),\n [merged]\n );\n\n return createElement(Context.Provider, { value }, children);\n}\n\ntype Props = Record<string, string>;\n\nexport type AnalyticsAPI = {\n event(eventName: string, pathOrProps?: string | Props, props?: Props): void;\n view(pathOrProps?: string | Props, props?: Props): void;\n};\n\nexport function useAnalytics(): AnalyticsAPI {\n const ctx = useRequiredContext('useAnalytics');\n const trackedPath = useTrackedPath(ctx.config);\n\n const event = useCallback(\n (eventName: string, pathOrProps?: string | Props, props?: Props) => {\n const targetPath = typeof pathOrProps === 'string' ? pathOrProps : trackedPath;\n const eventProps = typeof pathOrProps === 'object' ? pathOrProps : props;\n send(eventName, targetPath, ctx.config, eventProps);\n },\n [ctx, trackedPath]\n );\n\n const view = useCallback(\n (pathOrProps?: string | Props, props?: Props) => {\n const targetPath = typeof pathOrProps === 'string' ? pathOrProps : trackedPath;\n const viewProps = typeof pathOrProps === 'object' ? pathOrProps : props;\n // Same page as the last pageview (auto or manual): skip, like the web tracker\n const page = normalizePath(targetPath);\n if (ctx.lastPathRef.current === page) return;\n ctx.lastPathRef.current = page;\n send('PageView', targetPath, ctx.config, viewProps);\n },\n [ctx, trackedPath]\n );\n\n return { event, view };\n}\n\n// TODO(page-scope): restore when page-scope detection is designed.\n/*\nexport function useAnalyticsPath(customPath: string): void {\n const ctx = useRequiredContext('useAnalyticsPath');\n const pathname = usePathname();\n useEffect(() => {\n const entry: Override = { realPath: pathname, customPath, source: 'hook' };\n ctx.overrideRef.current = entry;\n return () => {\n if (ctx.overrideRef.current === entry) ctx.overrideRef.current = null;\n };\n }, [ctx, pathname, customPath]);\n}\n\nexport type AnalyticsPathProps = { path: string };\n\nexport function AnalyticsPath({ path }: AnalyticsPathProps): null {\n const ctx = useRequiredContext('AnalyticsPath');\n const pathname = usePathname();\n useEffect(() => {\n const existing = ctx.overrideRef.current;\n if (existing && existing.realPath === pathname && existing.source === 'hook') return;\n const entry: Override = { realPath: pathname, customPath: path, source: 'component' };\n ctx.overrideRef.current = entry;\n return () => {\n if (ctx.overrideRef.current === entry) ctx.overrideRef.current = null;\n };\n }, [ctx, pathname, path]);\n return null;\n}\n\nexport function useAnalyticsProps(props: Record<string, string>): void {\n const ctx = useRequiredContext('useAnalyticsProps');\n const pathname = usePathname();\n useEffect(() => {\n const entry: PropsOverride = { realPath: pathname, props, source: 'hook' };\n ctx.propsOverrideRef.current = entry;\n return () => {\n if (ctx.propsOverrideRef.current === entry) ctx.propsOverrideRef.current = null;\n };\n }, [ctx, pathname, props]);\n}\n\nexport type AnalyticsPropsProps = Record<string, string>;\n\nexport function AnalyticsProps(props: AnalyticsPropsProps): null {\n const ctx = useRequiredContext('AnalyticsProps');\n const pathname = usePathname();\n useEffect(() => {\n const existing = ctx.propsOverrideRef.current;\n if (existing && existing.realPath === pathname && existing.source === 'hook') return;\n const entry: PropsOverride = { realPath: pathname, props, source: 'component' };\n ctx.propsOverrideRef.current = entry;\n return () => {\n if (ctx.propsOverrideRef.current === entry) ctx.propsOverrideRef.current = null;\n };\n }, [ctx, pathname, props]);\n return null;\n}\n*/\n\nfunction shouldSkipSend(config: InternalConfig): boolean {\n return isDevEnvironment() && !config.devmode;\n}\n\nfunction devLog(label: string, url: string, props?: Record<string, string>): void {\n let msg = `[onedollarstats]\\nEvent name: ${label}\\nEvent collected from: ${url}`;\n if (props && Object.keys(props).length > 0) {\n msg += `\\nProps: ${JSON.stringify(props, null, 2)}`;\n }\n console.log(msg);\n}\n\n// Same URL as the web tracker: leading slash, no trailing slash, no query or hash, browser-style encoding.\n// Plain strings on purpose: React Native's URL throws \"not implemented\" for most properties.\nfunction buildUrl(hostname: string, path: string): string {\n return `https://${hostname}${normalizePath(path)}`;\n}\n\n// The path part of buildUrl(), so paths that send the same URL count as the same page. Root is ''.\nfunction normalizePath(path: string): string {\n const bare = path.split(/[?#]/)[0];\n const withSlash = bare.startsWith('/') ? bare : `/${bare}`;\n // What browsers encode in a path; [ ] (route templates) and existing %XX stay as is\n const encoded = withSlash.replace(/[^\\x21-\\x7E]|[\"<>`{}]/gu, encodeChar);\n return encoded.replace(/\\/$/, '');\n}\n\nfunction encodeChar(char: string): string {\n try {\n return encodeURIComponent(char);\n } catch {\n return ''; // lone surrogate\n }\n}\n\n// Longer GET URLs can get cut by proxies and CDNs\nconst SAFE_GET_THRESHOLD = 1500;\n\nfunction send(\n eventName: string,\n path: string,\n config: InternalConfig,\n props?: Record<string, string>\n): void {\n if (shouldSkipSend(config)) return;\n const url = buildUrl(config.hostname, path);\n if (config.devmode) devLog(eventName, url, props);\n\n const body = JSON.stringify({\n u: url,\n e: [{ t: eventName, ...(props && { p: props }) }],\n debug: config.devmode\n });\n\n if (Platform.OS === 'web') {\n sendWeb(config.collectorUrl, body);\n } else {\n sendNative(config.collectorUrl, body);\n }\n}\n\n// RN has no sendBeacon or keepalive, so an event sent while the app closes can be lost\nfunction sendNative(collectorUrl: string, body: string): void {\n const userAgent = nativeUserAgent();\n fetch(collectorUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...(userAgent && { 'User-Agent': userAgent }) },\n body\n }).catch(() => {}); // analytics must never crash the app\n}\n\n// Native fetch sends okhttp/x (Android) or CFNetwork (iOS), which the collector's UA parser reads as\n// an unknown OS and device. This browser-shaped UA parses as iOS 17.4 / iPhone or Android 14 / Pixel 8.\n// \"; Apple\" is what makes the parser see an iPad as a tablet.\nfunction nativeUserAgent(): string | undefined {\n try {\n if (Platform.OS === 'ios') {\n const version = uaToken(Platform.Version).replace(/\\./g, '_');\n const device = Platform.isPad ? 'iPad' : 'iPhone';\n // Without a version, \"CPU iPhone OS like Mac OS X\" parses as macOS, so leave the OS out\n const os = version ? `; CPU ${Platform.isPad ? '' : 'iPhone '}OS ${version} like Mac OS X` : '';\n return `Mozilla/5.0 (${device}${os}; Apple) ${UA_MARKER}`;\n }\n if (Platform.OS === 'android') {\n const { Release, Model } = Platform.constants;\n const release = uaToken(Release);\n const details = ['Linux', release ? `Android ${release}` : 'Android', uaToken(Model)].filter(Boolean);\n return `Mozilla/5.0 (${details.join('; ')}) ${UA_MARKER}`;\n }\n } catch {\n // Missing platform constants: keep the default UA rather than fail the event\n }\n return undefined;\n}\n\n// Lets the collector tell events from this package apart\nconst UA_MARKER = 'onedollarstats-expo';\n\n// Model names can hold anything; ; and ) would break the UA's structure\nfunction uaToken(value: unknown): string {\n return String(value ?? '').replace(/[^\\w .-]/g, '').trim().slice(0, 64);\n}\n\n// Pixel GET first, then sendBeacon, then fetch\nfunction sendWeb(collectorUrl: string, body: string): void {\n // btoa only takes Latin-1, so encode to UTF-8 bytes first\n const bytes = new TextEncoder().encode(body);\n const bin = String.fromCharCode(...bytes);\n const payloadBase64 = btoa(bin);\n\n if (payloadBase64.length <= SAFE_GET_THRESHOLD) {\n const img = new Image(1, 1);\n img.onerror = () => sendBeaconOrFetch(collectorUrl, body);\n img.src = `${collectorUrl}?data=${payloadBase64}`;\n return;\n }\n\n sendBeaconOrFetch(collectorUrl, body);\n}\n\nfunction sendBeaconOrFetch(collectorUrl: string, body: string): void {\n if (typeof navigator !== 'undefined' && navigator.sendBeacon?.(collectorUrl, body)) {\n return;\n }\n\n fetch(collectorUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n keepalive: true\n }).catch(() => {});\n}\n\n// '/admin' matches '/admin' and '/admin/users', not '/administrator'\nfunction pathMatches(path: string, prefix: string): boolean {\n return path === prefix || path.startsWith(prefix.endsWith('/') ? prefix : prefix + '/');\n}\n\n// includePages wins: excludePages is ignored when both are set (web applies both)\nfunction isExcluded(path: string, config: InternalConfig): boolean {\n if (config.includePages?.length) {\n return !config.includePages.some(p => pathMatches(path, p));\n }\n if (config.excludePages?.length) {\n return config.excludePages.some(p => pathMatches(path, p));\n }\n return false;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/index.js
CHANGED
|
@@ -330,8 +330,11 @@ UTM: ${data.utm}`;
|
|
|
330
330
|
Object.assign(collectedProps, props);
|
|
331
331
|
}
|
|
332
332
|
const viewProps = Object.keys(collectedProps).length > 0 ? collectedProps : void 0;
|
|
333
|
+
if (checkBlock && !shouldTrackPath(viewPath, this.config)) {
|
|
334
|
+
this.lastPage = null;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
333
337
|
if (this.lastPage === viewPath) return;
|
|
334
|
-
if (checkBlock && !shouldTrackPath(viewPath, this.config)) return;
|
|
335
338
|
this.lastPage = viewPath;
|
|
336
339
|
const utm = parseUtmParams(new URLSearchParams(location.search));
|
|
337
340
|
this.send({ type: "PageView", path: viewPath, props: viewProps, utm });
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/utils/environment.ts", "../src/utils/bot.ts", "../src/utils/merge-config.ts", "../src/utils/parse-utm-params.ts", "../src/utils/props-parser.ts", "../src/utils/resolve-path.ts", "../src/utils/should-track.ts", "../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["export const getEnvironment = (): {\n isLocalhost: boolean;\n isDesktop: boolean;\n} => {\n // Electron, Tauri, Wails, Neutralino, NW.js\n // typeof checks instead of `in`: elements with a matching id are exposed on window too\n const isDesktop =\n /Electron\\//.test(navigator.userAgent) ||\n typeof (window as any).__TAURI_INTERNALS__?.invoke === \"function\" ||\n typeof (window as any).__TAURI_IPC__ === \"function\" ||\n /^(tauri|wails):$/.test(location.protocol) ||\n /^(tauri|wails)\\.localhost$/.test(location.hostname) ||\n typeof (window as any).NL_VERSION === \"string\" ||\n typeof (window as any).nw?.App === \"object\";\n\n return {\n // Desktop apps are served from localhost/file:// in production too, so they are never \"localhost\"\n isLocalhost:\n !isDesktop &&\n ((/^localhost$|^127(\\.[0-9]+){0,2}\\.[0-9]+$|^\\[::1?\\]$/.test(location.hostname) &&\n //TODO revisit\n (location.protocol === \"http:\" || location.protocol === \"https:\")) ||\n (location.protocol === \"file:\" && !navigator.userAgent.toLowerCase().includes(\"electron\"))),\n isDesktop\n };\n};\nexport const isClient = (): boolean => {\n try {\n // Basic checks for window and document\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return false;\n\n // Check for navigator safely\n const ua = typeof navigator !== \"undefined\" ? navigator.userAgent : \"\";\n // Node's UA starts with \"Node.js/\"; a bare \"node\" match would also hit desktop apps named e.g. \"NodeNotes\"\n if (/^Node\\.js\\/|jsdom/i.test(ua)) return false;\n return true;\n } catch {\n return false;\n }\n};\n", "/**\n * Bot & crawler detection module.\n *\n * Detects:\n * - Crawlers and services that run JS: search, social, AI, page speed, prerender\n * - Headless browsers (Puppeteer, Playwright, Selenium, PhantomJS)\n * - General automation tools via navigator.webdriver and injected globals\n *\n * A wrong match silently drops a real visitor, so only signals humans can't trip\n * belong here. Patched browser APIs are not one: privacy extensions do that.\n *\n * TODO: filter the bots this can't catch in the collector. These look like real users in the browser:\n * - Stealth bots (puppeteer-extra stealth) and headless Chrome with a spoofed UA\n * - TikTok's DM link scanner: a real Android WebView from datacenter IPs (seen from Datacamp\n * Limited, AS212238), sometimes with a fake iPhone UA\n * First step: drop events with TikTok UA tokens (BytedanceWebview, musical_ly, trill_) from\n * hosting-provider ASNs, after 1\u20132 weeks of only counting matches. Blocking all datacenter IPs\n * would also drop real VPN users (Datacamp hosts VPN servers), so measure that first.\n */\n\nimport { getEnvironment } from \"./environment\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type BotKind =\n | \"headless\" // Headless Chrome/Firefox, PhantomJS\n | \"automation\" // Selenium, Puppeteer, Playwright (non-headless)\n | \"unknown_bot\" // Signals say bot, but can't classify further\n | \"human\"; // No bot signals detected\n\nexport interface BotSignals {\n /** navigator.userAgent matched a known bot pattern. */\n userAgentBot: boolean;\n /** navigator.webdriver is true. */\n webdriver: boolean;\n /** Headless browser indicators detected. */\n headless: boolean;\n /** Automation globals found on window. */\n automationGlobals: string[];\n}\n\nexport interface BotDetectionResult {\n /** True when any bot signal fires. */\n isBot: boolean;\n /** Classified category of the detected bot. */\n botKind: BotKind;\n /** Individual signal results for debugging / logging. */\n signals: BotSignals;\n}\n\n// ---------------------------------------------------------------------------\n// Known bot UA patterns\n// ---------------------------------------------------------------------------\n\n// Single alternation. The generic tokens (bot, crawl, spider, slurp, fetch,\n// archiver) subsume most named crawlers; only names that don't contain one of\n// those tokens are listed explicitly. Clients that can't run JS (curl, Scrapy,\n// python-requests, ...) never load this script, so they aren't listed.\n//\n// \"bot\" must end a word and not follow \"u\": the CUBOT phone brand and app names\n// like \"botanica/1.0.0\" are real visitors. Don't use a lookbehind here: Safari\n// before 16.4 can't parse it, which would break the whole script.\nconst BOT_UA_PATTERN =\n /Google-InspectionTool|Google-Read-Aloud|Google-PageRenderer|GoogleOther|Mediapartners-Google|BingPreview|Yeti\\/|ChatGPT-User|Claude-User|Perplexity-User|MistralAI-User|anthropic-ai|cohere-ai|facebookexternalhit|vkShare|Embedly|^WhatsApp\\/|Chrome-Lighthouse|GTmetrix|PTST\\/|StatusCake|Site24x7|DatadogSynthetics|Checkly|Siteimprove|Prerender|Rendertron|HeadlessChrome|PhantomJS|Selenium|Puppeteer|Cypress\\/|jsdom\\/|(?:^|[^u])bot(?![a-z])|crawl|spider|slurp|fetch|archiver/i;\n\n// ---------------------------------------------------------------------------\n// Automation globals injected by common frameworks\n// ---------------------------------------------------------------------------\n\n// Don't add plain \"phantom\": the Phantom crypto wallet extension injects window.phantom\nconst AUTOMATION_GLOBALS = [\n // Selenium\n \"__selenium_unwrapped\",\n \"__selenium_evaluate\",\n \"__webdriver_evaluate\",\n \"__webdriver_script_fn\",\n \"__webdriver_script_func\",\n \"__webdriver_script_function\",\n \"__fxdriver_evaluate\",\n \"__fxdriver_unwrapped\",\n \"_Selenium_IDE_Recorder\",\n // Puppeteer / CDP\n \"__puppeteer_evaluation_script__\",\n // PhantomJS\n \"callPhantom\",\n \"_phantom\",\n // Nightmare.js\n \"__nightmare\",\n // Playwright (injects page.exposeFunction bindings)\n \"__playwright\",\n \"__pw_manual\",\n // CasperJS\n \"__casper\",\n // TestCafe\n \"__testcafe\",\n // Cypress\n \"Cypress\",\n // Chrome automation\n \"domAutomation\",\n \"domAutomationController\",\n] as const;\n\n// ---------------------------------------------------------------------------\n// Core detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the current browser context is a bot or crawler.\n * Synchronous and lightweight \u2014 no async APIs needed.\n */\nexport function detectBot(): BotDetectionResult {\n const signals = collectBotSignals();\n const { isDesktop } = getEnvironment();\n\n // Desktop webviews: the app name is in the UA (e.g. \"mybot/1.0.0\") and headless heuristics don't apply\n if (isDesktop) signals.userAgentBot = signals.headless = false;\n\n const isBot = signals.userAgentBot || signals.webdriver || signals.headless || signals.automationGlobals.length > 0;\n\n let botKind: BotKind = \"human\";\n if (isBot) {\n if (signals.headless) {\n botKind = \"headless\";\n } else if (signals.webdriver || signals.automationGlobals.length > 0) {\n botKind = \"automation\";\n } else {\n botKind = \"unknown_bot\";\n }\n }\n\n return { isBot, botKind, signals };\n}\n\nexport function shouldBlock(devmode: boolean): boolean {\n if (getEnvironment().isLocalhost && !devmode) return true;\n\n const { isBot, botKind, signals } = detectBot();\n if (isBot && devmode) console.log(`[onedollarstats]\\nEvent blocked as bot (${botKind}): ${JSON.stringify(signals)}`);\n return isBot;\n}\n\n// ---------------------------------------------------------------------------\n// Signal collectors\n// ---------------------------------------------------------------------------\n\nfunction collectBotSignals(): BotSignals {\n return {\n userAgentBot: detectUserAgentBot(),\n webdriver: detectWebdriver(),\n headless: detectHeadless(),\n automationGlobals: detectAutomationGlobals(),\n };\n}\n\n/** Check UA string against known bot patterns. */\nfunction detectUserAgentBot(): boolean {\n const ua = navigator.userAgent || \"\";\n if (!ua) return true;\n return BOT_UA_PATTERN.test(ua);\n}\n\n/** navigator.webdriver is set by WebDriver-based automation. */\nfunction detectWebdriver(): boolean {\n return !!(navigator as any).webdriver;\n}\n\n/**\n * Headless browser indicators. Browser feature heuristics don't work here, real users trip them:\n * - no window.chrome: Android WebViews, which is what in-app browsers run on\n * - denied notifications with no plugins: Chrome set to download PDFs, on any http site\n */\nfunction detectHeadless(): boolean {\n return /HeadlessChrome/.test(navigator.userAgent);\n}\n\n/** Detect automation framework globals on window. */\nfunction detectAutomationGlobals(): string[] {\n const w = window as any;\n return AUTOMATION_GLOBALS.filter((key) => {\n try {\n // Own properties only: elements with a matching id are exposed through window's prototype chain\n return Object.prototype.hasOwnProperty.call(w, key) && w[key] !== undefined;\n } catch {\n return false;\n }\n }) as string[];\n}\n", "import type { AnalyticsConfig, InternalAnalyticsConfig } from \"../types\";\nimport { getEnvironment } from \"./environment\";\n\nexport const defaultConfig: InternalAnalyticsConfig = {\n hostname: null,\n devmode: false,\n collectorUrl: \"https://collector.onedollarstats.com/events\",\n hashRouting: false,\n autocollect: true,\n excludePages: [],\n includePages: [],\n};\n\nexport const mergeConfig = (userConfig: AnalyticsConfig = {}): InternalAnalyticsConfig => {\n const { isLocalhost, isDesktop } = getEnvironment();\n\n // Desktop can't tell dev from production by URL, so devmode is used as passed\n // TODO: auto-detect desktop dev: process.env.NODE_ENV === \"development\" (replaced by the app's bundler),\n // Tauri/Electron served from http://localhost, Electron file:// without app.asar; then block unless devmode like web localhost\n const devmode =\n !isLocalhost && !isDesktop ? false : (userConfig.devmode ?? !!userConfig.trackLocalhostAs);\n\n let hostname: string | null;\n if (userConfig.hostname) {\n const trimmed = userConfig.hostname.trim();\n hostname = trimmed || null;\n } else if (devmode && userConfig.trackLocalhostAs) {\n hostname = userConfig.trackLocalhostAs;\n } else {\n hostname = null;\n }\n\n // Merge default config, user config, and computed values\n return { ...defaultConfig, ...userConfig, hostname, devmode };\n};\n", "export function parseUtmParams(urlSearchParams: URLSearchParams) {\n const utm: Record<string, string> = {};\n const keys = [\"utm_campaign\", \"utm_source\", \"utm_medium\", \"utm_term\", \"utm_content\"] as const;\n\n for (const key of keys) {\n const raw = urlSearchParams.get(key);\n if (!raw) continue;\n\n const decoded = decodeAndTrim(raw);\n if (decoded) {\n utm[key] = decoded;\n }\n }\n\n return utm;\n}\n\nfunction decodeAndTrim(value: string): string {\n let decoded = value;\n let previous = \"\";\n\n while (decoded !== previous) {\n previous = decoded;\n try {\n decoded = decodeURIComponent(decoded);\n } catch {\n return decoded.trim();\n }\n }\n\n return decoded.trim();\n}\n", "export const parseProps = (propsString: string): Record<string, string> | undefined => {\n if (!propsString) return undefined;\n // \"key1=value1;key2=value2\"\n\n const splittedProps = propsString.split(\";\");\n const propsObj: Record<string, string> = {};\n\n for (const keyValueString of splittedProps) {\n const keyValuePair = keyValueString.split(\"=\").map((el) => el.trim());\n if (keyValuePair.length !== 2 || keyValuePair[0] === \"\" || keyValuePair[1] === \"\") continue;\n // @ts-ignore\n propsObj[keyValuePair[0]] = keyValuePair[1];\n }\n\n return Object.keys(propsObj).length === 0 ? undefined : propsObj;\n};\n", "export const resolvePath = (pathOrProps?: string, hashRouting: boolean = false): string => {\n if (pathOrProps) return pathOrProps;\n\n const sources = [\n { value: document.body?.getAttribute(\"data-s-path\"), name: \"data-s-path\" },\n { value: document.body?.getAttribute(\"data-s:path\"), name: \"data-s:path\" },\n { value: document.querySelector('meta[name=\"stonks-path\"]')?.getAttribute(\"content\"), name: \"meta[stonks-path]\" }\n ];\n\n // Only keep sources that actually exist\n const existing = sources.filter(({ value }) => value);\n\n if (existing.length > 1) {\n console.warn(\"[onedollarstats] Multiple path sources found. Using priority order:\", existing.map(({ name }) => name).join(\" > \"));\n }\n\n // Hash route as path: \"#/users?tab=1\" -> \"/users\"\n const hashPath = hashRouting && location.hash.replace(/^#!?\\/?/, \"/\").split(\"?\")[0];\n\n // file:// pathname is a file system path (desktop apps), never send it\n const urlPath = location.protocol === \"file:\" ? \"/\" : location.pathname;\n\n // Return first available value, fallback to hash route or URL path\n return existing[0]?.value ?? (hashPath || urlPath);\n};\n", "import type { InternalAnalyticsConfig } from \"../types\";\n\nconst matchesPattern = (path: string, pattern: string): boolean => {\n // Escape special regex characters except '*' which becomes '.*'\n const escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*/g, \".*\");\n return new RegExp(`^${escaped}$`).test(path);\n};\n\nexport const shouldTrackPath = (path: string, config: InternalAnalyticsConfig): boolean => {\n // Exclude pages first\n if (config.excludePages.some((pattern) => matchesPattern(path, pattern))) return false;\n // If includePages is defined, only allow matching paths\n if (config.includePages.length && !config.includePages.some((pattern) => matchesPattern(path, pattern))) return false;\n return true;\n};\n", "import type { AnalyticsConfig, BaseProps, BodyToSend, Event, InternalAnalyticsConfig, ViewArguments } from \"./types\";\nimport { shouldBlock } from \"./utils/bot\";\nimport { getEnvironment, isClient } from \"./utils/environment\";\nimport { mergeConfig } from \"./utils/merge-config\";\nimport { parseUtmParams } from \"./utils/parse-utm-params\";\nimport { parseProps } from \"./utils/props-parser\";\nimport { resolvePath } from \"./utils/resolve-path\";\nimport { shouldTrackPath } from \"./utils/should-track\";\n\ndeclare const DEBUG_SCRIPT_URL: string;\n\nclass AnalyticsTracker {\n private static instance: AnalyticsTracker | null = null;\n\n private autocollectSetupDone = false;\n private config: InternalAnalyticsConfig;\n private lastPage: string | null = null;\n\n public static getInstance(userConfig: AnalyticsConfig = {}): AnalyticsTracker {\n // Fresh no-op instance for SSR\n if (!isClient()) return new AnalyticsTracker(userConfig);\n\n if (!AnalyticsTracker.instance) {\n AnalyticsTracker.instance = new AnalyticsTracker(userConfig);\n }\n return AnalyticsTracker.instance;\n }\n\n private constructor(userConfig: AnalyticsConfig = {}) {\n this.config = mergeConfig(userConfig);\n\n // Skip setup in non-client environments\n if (!isClient()) return;\n\n const { isLocalhost } = getEnvironment();\n\n // Debug log on localhost\n if (isLocalhost && this.config.devmode && this.config.hostname) {\n console.log(`[onedollarstats]\\nOneDollarStats connected! Tracking localhost as ${this.config.hostname}`);\n\n // Set up debug modal loading: store config + queue, then dynamically load the debug script\n window.__stonksDebugConfig = { hostname: this.config.hostname, collectorUrl: this.config.collectorUrl };\n window.__stonksModalQueue = [];\n window.__stonksModalReady = false;\n\n const debugScript = document.createElement(\"script\");\n debugScript.src = DEBUG_SCRIPT_URL;\n debugScript.onerror = () => {\n // If the debug script fails to load, mark as ready so queued events are not stuck\n window.__stonksModalReady = true;\n };\n document.head.appendChild(debugScript);\n }\n\n // Auto-start autocollect (always set up listeners; handlePageView checks config)\n this.setupAutocollect();\n }\n\n private async sendWithBeaconOrFetch(stringifiedBody: string, callback: (success: boolean) => void): Promise<void> {\n // First fallback: try sendBeacon\n if (navigator.sendBeacon?.(this.config.collectorUrl, stringifiedBody)) {\n callback(true);\n return;\n }\n\n // Second fallback: use fetch() with keepalive\n fetch(this.config.collectorUrl, {\n method: \"POST\",\n body: stringifiedBody,\n headers: { \"Content-Type\": \"application/json\" },\n keepalive: true\n })\n .then(({ ok }) => callback(ok))\n .catch((err: Error) => {\n console.error(\"[onedollarstats] fetch() failed:\", err.message);\n callback(false);\n });\n }\n\n // Handles localhost replacement, referrer, UTM parameters, and debug mode.\n // Uses img beacon then `navigator.sendBeacon` if available, otherwise falls back to `fetch`.\n private async send(data: Event): Promise<void> {\n // Same gate as the CDN script: localhost without devmode, headless browsers and bots never send\n if (shouldBlock(this.config.devmode)) return;\n\n const urlToSend = new URL(this.config.hostname ? `https://${this.config.hostname}${location.pathname}` : location.href);\n\n // Clean query string unless UTM is explicitly provided\n urlToSend.search = \"\";\n urlToSend.hash = \"\";\n if (data.path) urlToSend.pathname = data.path;\n\n const cleanUrl = urlToSend.href.replace(/\\/$/, \"\");\n\n // Determine referrer\n let referrer: string | undefined = data.referrer;\n try {\n if (!referrer && document.referrer && document.referrer !== \"null\") {\n const referrerURL = new URL(document.referrer);\n // Skip internal (tracked or real host) and non-http referrers (file://, app://, tauri://)\n if (/^https?:$/.test(referrerURL.protocol) && referrerURL.hostname !== urlToSend.hostname && referrerURL.hostname !== location.hostname) referrer = referrerURL.href;\n }\n } catch {} // ignore malformed referrer\n\n // Build request body\n const body: BodyToSend = {\n u: cleanUrl,\n e: [\n {\n t: data.type,\n h: this.config.hashRouting,\n r: referrer,\n p: data.props\n }\n ],\n debug: this.config.devmode\n };\n\n if (data.utm && Object.keys(data.utm).length > 0) body.qs = data.utm;\n\n if (body.debug) {\n let logMessage = `[onedollarstats]\\nEvent name: ${data.type}\\nEvent collected from: ${cleanUrl}`;\n if (data.props && Object.keys(data.props).length > 0) logMessage += `\\nProps: ${JSON.stringify(data.props, null, 2)}`;\n if (referrer) logMessage += `\\nReferrer: ${referrer}`;\n if (this.config.hashRouting) logMessage += `\\nHashRouting: ${this.config.hashRouting}`;\n if (data.utm && Object.keys(data.utm).length > 0) logMessage += `\\nUTM: ${data.utm}`;\n\n console.log(logMessage);\n }\n\n // Prepare the event payload\n const stringifiedBody = JSON.stringify(body);\n\n // Encode for safe inclusion in a query string (UTF-8 \u2192 Base64)\n const bytes = new TextEncoder().encode(stringifiedBody); // UTF-8 \u2192 bytes\n const bin = String.fromCharCode(...bytes); // bytes \u2192 binary string\n const payloadBase64 = btoa(bin); // binary \u2192 Base64\n\n const safeGetThreshold = 1500; // limit for query-string-containing URLs\n const tryImageBeacon = payloadBase64.length <= safeGetThreshold;\n\n const onComplete = (success: boolean) => {\n const message = `${data.type} ${success ? \"sent\" : \"failed to send\"}`;\n if (window.__stonksModalReady) {\n window.__stonksModalLog?.(message, success);\n } else {\n window.__stonksModalQueue?.push([message, success]);\n }\n };\n\n if (tryImageBeacon) {\n // Send via image beacon\n const img = new Image(1, 1);\n\n img.onload = () => onComplete(true);\n // If loading image fails (server unavailable, blocked, etc.)\n img.onerror = () => this.sendWithBeaconOrFetch(stringifiedBody, onComplete);\n\n // Primary attempt: send data via image beacon (GET request with query string)\n img.src = `${this.config.collectorUrl}?data=${payloadBase64}`;\n } else await this.sendWithBeaconOrFetch(stringifiedBody, onComplete);\n }\n\n // Prevents duplicate pageviews and respects include/exclude page rules. Automatically parses UTM parameters from URL.\n private trackPageView({ path, props }: ViewArguments, checkBlock: boolean = false) {\n if (!isClient()) return;\n\n const viewPath = resolvePath(path, this.config.hashRouting);\n\n // Collect props from DOM attributes\n const collectedProps: Record<string, string> = {};\n const elements = document.querySelectorAll(\"[data-s\\\\:view-props], [data-s-view-props]\");\n\n for (const el of Array.from(elements)) {\n const propsString = el.getAttribute(\"data-s-view-props\") || el.getAttribute(\"data-s:view-props\");\n if (!propsString) continue;\n const parsedProps = parseProps(propsString);\n Object.assign(collectedProps, parsedProps);\n }\n\n // Collect props from meta tag (overrides DOM attributes)\n const metaViewProps = document\n .querySelector('meta[name=\"stonks-props\"]')\n ?.getAttribute(\"content\");\n if (metaViewProps) {\n Object.assign(collectedProps, parseProps(metaViewProps));\n }\n\n // Explicit props override everything\n if (props) {\n Object.assign(collectedProps, props);\n }\n\n const viewProps = Object.keys(collectedProps).length > 0 ? collectedProps : undefined;\n\n // Skip duplicate pageviews or excluded pages\n if (this.lastPage === viewPath) return;\n\n // Skip page if checkBlock is true and the path should be excluded\n if (checkBlock && !shouldTrackPath(viewPath, this.config)) return;\n\n this.lastPage = viewPath;\n\n const utm = parseUtmParams(new URLSearchParams(location.search));\n this.send({ type: \"PageView\", path: viewPath, props: viewProps, utm });\n }\n\n /**\n * Tracks a custom event.\n * Can accept path string or a props object.\n *\n * @param eventName Name of the event to track.\n * @param pathOrProps Optional path string or props object.\n * @param props Optional props object if path string is provided.\n */\n public async event(eventName: string, pathOrProps?: string | BaseProps, rawProps?: BaseProps) {\n if (!isClient()) return;\n\n const path = resolvePath(typeof pathOrProps === \"string\" ? pathOrProps : undefined, this.config.hashRouting);\n const props = typeof pathOrProps === \"object\" ? pathOrProps : rawProps;\n\n this.send({ type: eventName, path, props });\n }\n\n /**\n * Records a page view.\n * Can accept path string or a props object.\n *\n * @param pathOrProps Optional path string or props object.\n * @param props Optional props when first arg is a path string.\n */\n public async view(pathOrProps?: string | BaseProps, props?: BaseProps) {\n if (!isClient()) return;\n\n const args: ViewArguments = {};\n\n if (typeof pathOrProps === \"string\") {\n args.path = pathOrProps;\n args.props = props;\n } else if (typeof pathOrProps === \"object\") {\n args.props = pathOrProps;\n }\n\n this.trackPageView(args);\n }\n\n /**\n * Installs global DOM/window listeners exactly once for:\n * - visibilitychange\n * - history.pushState\n * - popstate\n * - hashchange\n * - click autocapture for elements annotated with `data-s:event` & `data-s-event`\n *\n */\n private setupAutocollect() {\n if (!isClient() || this.autocollectSetupDone) return;\n this.autocollectSetupDone = true;\n\n const handlePageView = () => {\n // Check meta tag and body attribute for collection control\n const metaCollect = document\n .querySelector('meta[name=\"stonks-collect\"]')\n ?.getAttribute(\"content\");\n const bodyCollect =\n document.body?.getAttribute(\"data-s-collect\") ||\n document.body?.getAttribute(\"data-s:collect\");\n\n // Explicitly disabled\n if (metaCollect === \"false\" || bodyCollect === \"false\") {\n this.lastPage = null;\n return;\n }\n\n // If autocollect is off, only collect if explicitly enabled via meta/body\n if (!this.config.autocollect && metaCollect !== \"true\" && bodyCollect !== \"true\") {\n this.lastPage = null;\n return;\n }\n\n this.trackPageView({}, true);\n };\n\n // visibilitychange\n const onVisibility = () => {\n if (document.visibilityState === \"visible\") handlePageView();\n };\n document.addEventListener(\"visibilitychange\", onVisibility);\n\n // pushState\n const origPush = history.pushState.bind(history);\n history.pushState = (...args) => {\n origPush(...args);\n requestAnimationFrame(() => {\n handlePageView();\n });\n };\n\n // popstate\n window.addEventListener(\"popstate\", handlePageView);\n\n // hashchange\n window.addEventListener(\"hashchange\", handlePageView);\n\n // click autocapture\n const onClick: EventListener = (ev: Event) => {\n const clickEvent = ev as MouseEvent;\n if (clickEvent.type === \"auxclick\" && clickEvent.button !== 1) return;\n\n const target = clickEvent.target as Element | null;\n if (!target) return;\n\n // Check if inside <a> or <button>\n const insideInteractive = !!target.closest(\"a, button\");\n\n let el: Element | null = target;\n let depth = 0;\n\n while (el) {\n const eventName = el.getAttribute(\"data-s-event\") || el.getAttribute(\"data-s:event\");\n if (eventName) {\n const propsAttr = el.getAttribute(\"data-s-event-props\") || el.getAttribute(\"data-s:event-props\");\n const props = propsAttr ? parseProps(propsAttr) : undefined;\n const path = el.getAttribute(\"data-s-event-path\") || el.getAttribute(\"data-s:event-path\") || undefined;\n\n if ((path && !shouldTrackPath(path, this.config)) || !shouldTrackPath(resolvePath(undefined, this.config.hashRouting), this.config)) {\n return;\n }\n\n this.event(eventName, path ?? props, props);\n return;\n }\n\n el = el.parentElement;\n depth++;\n\n // If not in <a>/<button>, stop after 3 levels\n if (!insideInteractive && depth >= 3) break;\n }\n };\n\n document.addEventListener(\"click\", onClick);\n\n // Fire initial pageview if already visible\n if (document.visibilityState === \"visible\") handlePageView();\n }\n}\n\nexport const configure = (userConfig: AnalyticsConfig = {}) => {\n AnalyticsTracker.getInstance(userConfig);\n};\n\nexport const event = async (eventName: string, pathOrProps?: string | BaseProps, props?: BaseProps) => {\n const instance = AnalyticsTracker.getInstance();\n await instance.event(eventName, pathOrProps, props);\n};\n\nexport const view = async (pathOrProps?: string | BaseProps, props?: BaseProps) => {\n const instance = AnalyticsTracker.getInstance();\n await instance.view(pathOrProps, props);\n};\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["export const getEnvironment = (): {\n isLocalhost: boolean;\n isDesktop: boolean;\n} => {\n // Electron, Tauri, Wails, Neutralino, NW.js\n // typeof checks instead of `in`: elements with a matching id are exposed on window too\n const isDesktop =\n /Electron\\//.test(navigator.userAgent) ||\n typeof (window as any).__TAURI_INTERNALS__?.invoke === \"function\" ||\n typeof (window as any).__TAURI_IPC__ === \"function\" ||\n /^(tauri|wails):$/.test(location.protocol) ||\n /^(tauri|wails)\\.localhost$/.test(location.hostname) ||\n typeof (window as any).NL_VERSION === \"string\" ||\n typeof (window as any).nw?.App === \"object\";\n\n return {\n // Desktop apps are served from localhost/file:// in production too, so they are never \"localhost\"\n isLocalhost:\n !isDesktop &&\n ((/^localhost$|^127(\\.[0-9]+){0,2}\\.[0-9]+$|^\\[::1?\\]$/.test(location.hostname) &&\n //TODO revisit\n (location.protocol === \"http:\" || location.protocol === \"https:\")) ||\n (location.protocol === \"file:\" && !navigator.userAgent.toLowerCase().includes(\"electron\"))),\n isDesktop\n };\n};\nexport const isClient = (): boolean => {\n try {\n // Basic checks for window and document\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return false;\n\n // Check for navigator safely\n const ua = typeof navigator !== \"undefined\" ? navigator.userAgent : \"\";\n // Node's UA starts with \"Node.js/\"; a bare \"node\" match would also hit desktop apps named e.g. \"NodeNotes\"\n if (/^Node\\.js\\/|jsdom/i.test(ua)) return false;\n return true;\n } catch {\n return false;\n }\n};\n", "/**\n * Bot & crawler detection module.\n *\n * Detects:\n * - Crawlers and services that run JS: search, social, AI, page speed, prerender\n * - Headless browsers (Puppeteer, Playwright, Selenium, PhantomJS)\n * - General automation tools via navigator.webdriver and injected globals\n *\n * A wrong match silently drops a real visitor, so only signals humans can't trip\n * belong here. Patched browser APIs are not one: privacy extensions do that.\n *\n * TODO: filter the bots this can't catch in the collector. These look like real users in the browser:\n * - Stealth bots (puppeteer-extra stealth) and headless Chrome with a spoofed UA\n * - TikTok's DM link scanner: a real Android WebView from datacenter IPs (seen from Datacamp\n * Limited, AS212238), sometimes with a fake iPhone UA\n * First step: drop events with TikTok UA tokens (BytedanceWebview, musical_ly, trill_) from\n * hosting-provider ASNs, after 1\u20132 weeks of only counting matches. Blocking all datacenter IPs\n * would also drop real VPN users (Datacamp hosts VPN servers), so measure that first.\n */\n\nimport { getEnvironment } from \"./environment\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type BotKind =\n | \"headless\" // Headless Chrome/Firefox, PhantomJS\n | \"automation\" // Selenium, Puppeteer, Playwright (non-headless)\n | \"unknown_bot\" // Signals say bot, but can't classify further\n | \"human\"; // No bot signals detected\n\nexport interface BotSignals {\n /** navigator.userAgent matched a known bot pattern. */\n userAgentBot: boolean;\n /** navigator.webdriver is true. */\n webdriver: boolean;\n /** Headless browser indicators detected. */\n headless: boolean;\n /** Automation globals found on window. */\n automationGlobals: string[];\n}\n\nexport interface BotDetectionResult {\n /** True when any bot signal fires. */\n isBot: boolean;\n /** Classified category of the detected bot. */\n botKind: BotKind;\n /** Individual signal results for debugging / logging. */\n signals: BotSignals;\n}\n\n// ---------------------------------------------------------------------------\n// Known bot UA patterns\n// ---------------------------------------------------------------------------\n\n// Single alternation. The generic tokens (bot, crawl, spider, slurp, fetch,\n// archiver) subsume most named crawlers; only names that don't contain one of\n// those tokens are listed explicitly. Clients that can't run JS (curl, Scrapy,\n// python-requests, ...) never load this script, so they aren't listed.\n//\n// \"bot\" must end a word and not follow \"u\": the CUBOT phone brand and app names\n// like \"botanica/1.0.0\" are real visitors. Don't use a lookbehind here: Safari\n// before 16.4 can't parse it, which would break the whole script.\nconst BOT_UA_PATTERN =\n /Google-InspectionTool|Google-Read-Aloud|Google-PageRenderer|GoogleOther|Mediapartners-Google|BingPreview|Yeti\\/|ChatGPT-User|Claude-User|Perplexity-User|MistralAI-User|anthropic-ai|cohere-ai|facebookexternalhit|vkShare|Embedly|^WhatsApp\\/|Chrome-Lighthouse|GTmetrix|PTST\\/|StatusCake|Site24x7|DatadogSynthetics|Checkly|Siteimprove|Prerender|Rendertron|HeadlessChrome|PhantomJS|Selenium|Puppeteer|Cypress\\/|jsdom\\/|(?:^|[^u])bot(?![a-z])|crawl|spider|slurp|fetch|archiver/i;\n\n// ---------------------------------------------------------------------------\n// Automation globals injected by common frameworks\n// ---------------------------------------------------------------------------\n\n// Don't add plain \"phantom\": the Phantom crypto wallet extension injects window.phantom\nconst AUTOMATION_GLOBALS = [\n // Selenium\n \"__selenium_unwrapped\",\n \"__selenium_evaluate\",\n \"__webdriver_evaluate\",\n \"__webdriver_script_fn\",\n \"__webdriver_script_func\",\n \"__webdriver_script_function\",\n \"__fxdriver_evaluate\",\n \"__fxdriver_unwrapped\",\n \"_Selenium_IDE_Recorder\",\n // Puppeteer / CDP\n \"__puppeteer_evaluation_script__\",\n // PhantomJS\n \"callPhantom\",\n \"_phantom\",\n // Nightmare.js\n \"__nightmare\",\n // Playwright (injects page.exposeFunction bindings)\n \"__playwright\",\n \"__pw_manual\",\n // CasperJS\n \"__casper\",\n // TestCafe\n \"__testcafe\",\n // Cypress\n \"Cypress\",\n // Chrome automation\n \"domAutomation\",\n \"domAutomationController\",\n] as const;\n\n// ---------------------------------------------------------------------------\n// Core detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the current browser context is a bot or crawler.\n * Synchronous and lightweight \u2014 no async APIs needed.\n */\nexport function detectBot(): BotDetectionResult {\n const signals = collectBotSignals();\n const { isDesktop } = getEnvironment();\n\n // Desktop webviews: the app name is in the UA (e.g. \"mybot/1.0.0\") and headless heuristics don't apply\n if (isDesktop) signals.userAgentBot = signals.headless = false;\n\n const isBot = signals.userAgentBot || signals.webdriver || signals.headless || signals.automationGlobals.length > 0;\n\n let botKind: BotKind = \"human\";\n if (isBot) {\n if (signals.headless) {\n botKind = \"headless\";\n } else if (signals.webdriver || signals.automationGlobals.length > 0) {\n botKind = \"automation\";\n } else {\n botKind = \"unknown_bot\";\n }\n }\n\n return { isBot, botKind, signals };\n}\n\nexport function shouldBlock(devmode: boolean): boolean {\n if (getEnvironment().isLocalhost && !devmode) return true;\n\n const { isBot, botKind, signals } = detectBot();\n if (isBot && devmode) console.log(`[onedollarstats]\\nEvent blocked as bot (${botKind}): ${JSON.stringify(signals)}`);\n return isBot;\n}\n\n// ---------------------------------------------------------------------------\n// Signal collectors\n// ---------------------------------------------------------------------------\n\nfunction collectBotSignals(): BotSignals {\n return {\n userAgentBot: detectUserAgentBot(),\n webdriver: detectWebdriver(),\n headless: detectHeadless(),\n automationGlobals: detectAutomationGlobals(),\n };\n}\n\n/** Check UA string against known bot patterns. */\nfunction detectUserAgentBot(): boolean {\n const ua = navigator.userAgent || \"\";\n if (!ua) return true;\n return BOT_UA_PATTERN.test(ua);\n}\n\n/** navigator.webdriver is set by WebDriver-based automation. */\nfunction detectWebdriver(): boolean {\n return !!(navigator as any).webdriver;\n}\n\n/**\n * Headless browser indicators. Browser feature heuristics don't work here, real users trip them:\n * - no window.chrome: Android WebViews, which is what in-app browsers run on\n * - denied notifications with no plugins: Chrome set to download PDFs, on any http site\n */\nfunction detectHeadless(): boolean {\n return /HeadlessChrome/.test(navigator.userAgent);\n}\n\n/** Detect automation framework globals on window. */\nfunction detectAutomationGlobals(): string[] {\n const w = window as any;\n return AUTOMATION_GLOBALS.filter((key) => {\n try {\n // Own properties only: elements with a matching id are exposed through window's prototype chain\n return Object.prototype.hasOwnProperty.call(w, key) && w[key] !== undefined;\n } catch {\n return false;\n }\n }) as string[];\n}\n", "import type { AnalyticsConfig, InternalAnalyticsConfig } from \"../types\";\nimport { getEnvironment } from \"./environment\";\n\nexport const defaultConfig: InternalAnalyticsConfig = {\n hostname: null,\n devmode: false,\n collectorUrl: \"https://collector.onedollarstats.com/events\",\n hashRouting: false,\n autocollect: true,\n excludePages: [],\n includePages: [],\n};\n\nexport const mergeConfig = (userConfig: AnalyticsConfig = {}): InternalAnalyticsConfig => {\n const { isLocalhost, isDesktop } = getEnvironment();\n\n // Desktop can't tell dev from production by URL, so devmode is used as passed\n // TODO: auto-detect desktop dev: process.env.NODE_ENV === \"development\" (replaced by the app's bundler),\n // Tauri/Electron served from http://localhost, Electron file:// without app.asar; then block unless devmode like web localhost\n const devmode =\n !isLocalhost && !isDesktop ? false : (userConfig.devmode ?? !!userConfig.trackLocalhostAs);\n\n let hostname: string | null;\n if (userConfig.hostname) {\n const trimmed = userConfig.hostname.trim();\n hostname = trimmed || null;\n } else if (devmode && userConfig.trackLocalhostAs) {\n hostname = userConfig.trackLocalhostAs;\n } else {\n hostname = null;\n }\n\n // Merge default config, user config, and computed values\n return { ...defaultConfig, ...userConfig, hostname, devmode };\n};\n", "export function parseUtmParams(urlSearchParams: URLSearchParams) {\n const utm: Record<string, string> = {};\n const keys = [\"utm_campaign\", \"utm_source\", \"utm_medium\", \"utm_term\", \"utm_content\"] as const;\n\n for (const key of keys) {\n const raw = urlSearchParams.get(key);\n if (!raw) continue;\n\n const decoded = decodeAndTrim(raw);\n if (decoded) {\n utm[key] = decoded;\n }\n }\n\n return utm;\n}\n\nfunction decodeAndTrim(value: string): string {\n let decoded = value;\n let previous = \"\";\n\n while (decoded !== previous) {\n previous = decoded;\n try {\n decoded = decodeURIComponent(decoded);\n } catch {\n return decoded.trim();\n }\n }\n\n return decoded.trim();\n}\n", "export const parseProps = (propsString: string): Record<string, string> | undefined => {\n if (!propsString) return undefined;\n // \"key1=value1;key2=value2\"\n\n const splittedProps = propsString.split(\";\");\n const propsObj: Record<string, string> = {};\n\n for (const keyValueString of splittedProps) {\n const keyValuePair = keyValueString.split(\"=\").map((el) => el.trim());\n if (keyValuePair.length !== 2 || keyValuePair[0] === \"\" || keyValuePair[1] === \"\") continue;\n // @ts-ignore\n propsObj[keyValuePair[0]] = keyValuePair[1];\n }\n\n return Object.keys(propsObj).length === 0 ? undefined : propsObj;\n};\n", "export const resolvePath = (pathOrProps?: string, hashRouting: boolean = false): string => {\n if (pathOrProps) return pathOrProps;\n\n const sources = [\n { value: document.body?.getAttribute(\"data-s-path\"), name: \"data-s-path\" },\n { value: document.body?.getAttribute(\"data-s:path\"), name: \"data-s:path\" },\n { value: document.querySelector('meta[name=\"stonks-path\"]')?.getAttribute(\"content\"), name: \"meta[stonks-path]\" }\n ];\n\n // Only keep sources that actually exist\n const existing = sources.filter(({ value }) => value);\n\n if (existing.length > 1) {\n console.warn(\"[onedollarstats] Multiple path sources found. Using priority order:\", existing.map(({ name }) => name).join(\" > \"));\n }\n\n // Hash route as path: \"#/users?tab=1\" -> \"/users\"\n const hashPath = hashRouting && location.hash.replace(/^#!?\\/?/, \"/\").split(\"?\")[0];\n\n // file:// pathname is a file system path (desktop apps), never send it\n const urlPath = location.protocol === \"file:\" ? \"/\" : location.pathname;\n\n // Return first available value, fallback to hash route or URL path\n return existing[0]?.value ?? (hashPath || urlPath);\n};\n", "import type { InternalAnalyticsConfig } from \"../types\";\n\nconst matchesPattern = (path: string, pattern: string): boolean => {\n // Escape special regex characters except '*' which becomes '.*'\n const escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*/g, \".*\");\n return new RegExp(`^${escaped}$`).test(path);\n};\n\nexport const shouldTrackPath = (path: string, config: InternalAnalyticsConfig): boolean => {\n // Exclude pages first\n if (config.excludePages.some((pattern) => matchesPattern(path, pattern))) return false;\n // If includePages is defined, only allow matching paths\n if (config.includePages.length && !config.includePages.some((pattern) => matchesPattern(path, pattern))) return false;\n return true;\n};\n", "import type { AnalyticsConfig, BaseProps, BodyToSend, Event, InternalAnalyticsConfig, ViewArguments } from \"./types\";\nimport { shouldBlock } from \"./utils/bot\";\nimport { getEnvironment, isClient } from \"./utils/environment\";\nimport { mergeConfig } from \"./utils/merge-config\";\nimport { parseUtmParams } from \"./utils/parse-utm-params\";\nimport { parseProps } from \"./utils/props-parser\";\nimport { resolvePath } from \"./utils/resolve-path\";\nimport { shouldTrackPath } from \"./utils/should-track\";\n\ndeclare const DEBUG_SCRIPT_URL: string;\n\nclass AnalyticsTracker {\n private static instance: AnalyticsTracker | null = null;\n\n private autocollectSetupDone = false;\n private config: InternalAnalyticsConfig;\n private lastPage: string | null = null;\n\n public static getInstance(userConfig: AnalyticsConfig = {}): AnalyticsTracker {\n // Fresh no-op instance for SSR\n if (!isClient()) return new AnalyticsTracker(userConfig);\n\n if (!AnalyticsTracker.instance) {\n AnalyticsTracker.instance = new AnalyticsTracker(userConfig);\n }\n return AnalyticsTracker.instance;\n }\n\n private constructor(userConfig: AnalyticsConfig = {}) {\n this.config = mergeConfig(userConfig);\n\n // Skip setup in non-client environments\n if (!isClient()) return;\n\n const { isLocalhost } = getEnvironment();\n\n // Debug log on localhost\n if (isLocalhost && this.config.devmode && this.config.hostname) {\n console.log(`[onedollarstats]\\nOneDollarStats connected! Tracking localhost as ${this.config.hostname}`);\n\n // Set up debug modal loading: store config + queue, then dynamically load the debug script\n window.__stonksDebugConfig = { hostname: this.config.hostname, collectorUrl: this.config.collectorUrl };\n window.__stonksModalQueue = [];\n window.__stonksModalReady = false;\n\n const debugScript = document.createElement(\"script\");\n debugScript.src = DEBUG_SCRIPT_URL;\n debugScript.onerror = () => {\n // If the debug script fails to load, mark as ready so queued events are not stuck\n window.__stonksModalReady = true;\n };\n document.head.appendChild(debugScript);\n }\n\n // Auto-start autocollect (always set up listeners; handlePageView checks config)\n this.setupAutocollect();\n }\n\n private async sendWithBeaconOrFetch(stringifiedBody: string, callback: (success: boolean) => void): Promise<void> {\n // First fallback: try sendBeacon\n if (navigator.sendBeacon?.(this.config.collectorUrl, stringifiedBody)) {\n callback(true);\n return;\n }\n\n // Second fallback: use fetch() with keepalive\n fetch(this.config.collectorUrl, {\n method: \"POST\",\n body: stringifiedBody,\n headers: { \"Content-Type\": \"application/json\" },\n keepalive: true\n })\n .then(({ ok }) => callback(ok))\n .catch((err: Error) => {\n console.error(\"[onedollarstats] fetch() failed:\", err.message);\n callback(false);\n });\n }\n\n // Handles localhost replacement, referrer, UTM parameters, and debug mode.\n // Uses img beacon then `navigator.sendBeacon` if available, otherwise falls back to `fetch`.\n private async send(data: Event): Promise<void> {\n // Same gate as the CDN script: localhost without devmode, headless browsers and bots never send\n if (shouldBlock(this.config.devmode)) return;\n\n const urlToSend = new URL(this.config.hostname ? `https://${this.config.hostname}${location.pathname}` : location.href);\n\n // Clean query string unless UTM is explicitly provided\n urlToSend.search = \"\";\n urlToSend.hash = \"\";\n if (data.path) urlToSend.pathname = data.path;\n\n const cleanUrl = urlToSend.href.replace(/\\/$/, \"\");\n\n // Determine referrer\n let referrer: string | undefined = data.referrer;\n try {\n if (!referrer && document.referrer && document.referrer !== \"null\") {\n const referrerURL = new URL(document.referrer);\n // Skip internal (tracked or real host) and non-http referrers (file://, app://, tauri://)\n if (/^https?:$/.test(referrerURL.protocol) && referrerURL.hostname !== urlToSend.hostname && referrerURL.hostname !== location.hostname) referrer = referrerURL.href;\n }\n } catch {} // ignore malformed referrer\n\n // Build request body\n const body: BodyToSend = {\n u: cleanUrl,\n e: [\n {\n t: data.type,\n h: this.config.hashRouting,\n r: referrer,\n p: data.props\n }\n ],\n debug: this.config.devmode\n };\n\n if (data.utm && Object.keys(data.utm).length > 0) body.qs = data.utm;\n\n if (body.debug) {\n let logMessage = `[onedollarstats]\\nEvent name: ${data.type}\\nEvent collected from: ${cleanUrl}`;\n if (data.props && Object.keys(data.props).length > 0) logMessage += `\\nProps: ${JSON.stringify(data.props, null, 2)}`;\n if (referrer) logMessage += `\\nReferrer: ${referrer}`;\n if (this.config.hashRouting) logMessage += `\\nHashRouting: ${this.config.hashRouting}`;\n if (data.utm && Object.keys(data.utm).length > 0) logMessage += `\\nUTM: ${data.utm}`;\n\n console.log(logMessage);\n }\n\n // Prepare the event payload\n const stringifiedBody = JSON.stringify(body);\n\n // Encode for safe inclusion in a query string (UTF-8 \u2192 Base64)\n const bytes = new TextEncoder().encode(stringifiedBody); // UTF-8 \u2192 bytes\n const bin = String.fromCharCode(...bytes); // bytes \u2192 binary string\n const payloadBase64 = btoa(bin); // binary \u2192 Base64\n\n const safeGetThreshold = 1500; // limit for query-string-containing URLs\n const tryImageBeacon = payloadBase64.length <= safeGetThreshold;\n\n const onComplete = (success: boolean) => {\n const message = `${data.type} ${success ? \"sent\" : \"failed to send\"}`;\n if (window.__stonksModalReady) {\n window.__stonksModalLog?.(message, success);\n } else {\n window.__stonksModalQueue?.push([message, success]);\n }\n };\n\n if (tryImageBeacon) {\n // Send via image beacon\n const img = new Image(1, 1);\n\n img.onload = () => onComplete(true);\n // If loading image fails (server unavailable, blocked, etc.)\n img.onerror = () => this.sendWithBeaconOrFetch(stringifiedBody, onComplete);\n\n // Primary attempt: send data via image beacon (GET request with query string)\n img.src = `${this.config.collectorUrl}?data=${payloadBase64}`;\n } else await this.sendWithBeaconOrFetch(stringifiedBody, onComplete);\n }\n\n // Prevents duplicate pageviews and respects include/exclude page rules. Automatically parses UTM parameters from URL.\n private trackPageView({ path, props }: ViewArguments, checkBlock: boolean = false) {\n if (!isClient()) return;\n\n const viewPath = resolvePath(path, this.config.hashRouting);\n\n // Collect props from DOM attributes\n const collectedProps: Record<string, string> = {};\n const elements = document.querySelectorAll(\"[data-s\\\\:view-props], [data-s-view-props]\");\n\n for (const el of Array.from(elements)) {\n const propsString = el.getAttribute(\"data-s-view-props\") || el.getAttribute(\"data-s:view-props\");\n if (!propsString) continue;\n const parsedProps = parseProps(propsString);\n Object.assign(collectedProps, parsedProps);\n }\n\n // Collect props from meta tag (overrides DOM attributes)\n const metaViewProps = document\n .querySelector('meta[name=\"stonks-props\"]')\n ?.getAttribute(\"content\");\n if (metaViewProps) {\n Object.assign(collectedProps, parseProps(metaViewProps));\n }\n\n // Explicit props override everything\n if (props) {\n Object.assign(collectedProps, props);\n }\n\n const viewProps = Object.keys(collectedProps).length > 0 ? collectedProps : undefined;\n\n // Excluded page: forget the last page, or A \u2192 excluded \u2192 A would look like a duplicate\n if (checkBlock && !shouldTrackPath(viewPath, this.config)) {\n this.lastPage = null;\n return;\n }\n\n // Skip duplicate pageviews\n if (this.lastPage === viewPath) return;\n\n this.lastPage = viewPath;\n\n const utm = parseUtmParams(new URLSearchParams(location.search));\n this.send({ type: \"PageView\", path: viewPath, props: viewProps, utm });\n }\n\n /**\n * Tracks a custom event.\n * Can accept path string or a props object.\n *\n * @param eventName Name of the event to track.\n * @param pathOrProps Optional path string or props object.\n * @param props Optional props object if path string is provided.\n */\n public async event(eventName: string, pathOrProps?: string | BaseProps, rawProps?: BaseProps) {\n if (!isClient()) return;\n\n const path = resolvePath(typeof pathOrProps === \"string\" ? pathOrProps : undefined, this.config.hashRouting);\n const props = typeof pathOrProps === \"object\" ? pathOrProps : rawProps;\n\n this.send({ type: eventName, path, props });\n }\n\n /**\n * Records a page view.\n * Can accept path string or a props object.\n *\n * @param pathOrProps Optional path string or props object.\n * @param props Optional props when first arg is a path string.\n */\n public async view(pathOrProps?: string | BaseProps, props?: BaseProps) {\n if (!isClient()) return;\n\n const args: ViewArguments = {};\n\n if (typeof pathOrProps === \"string\") {\n args.path = pathOrProps;\n args.props = props;\n } else if (typeof pathOrProps === \"object\") {\n args.props = pathOrProps;\n }\n\n this.trackPageView(args);\n }\n\n /**\n * Installs global DOM/window listeners exactly once for:\n * - visibilitychange\n * - history.pushState\n * - popstate\n * - hashchange\n * - click autocapture for elements annotated with `data-s:event` & `data-s-event`\n *\n */\n private setupAutocollect() {\n if (!isClient() || this.autocollectSetupDone) return;\n this.autocollectSetupDone = true;\n\n const handlePageView = () => {\n // Check meta tag and body attribute for collection control\n const metaCollect = document\n .querySelector('meta[name=\"stonks-collect\"]')\n ?.getAttribute(\"content\");\n const bodyCollect =\n document.body?.getAttribute(\"data-s-collect\") ||\n document.body?.getAttribute(\"data-s:collect\");\n\n // Explicitly disabled\n if (metaCollect === \"false\" || bodyCollect === \"false\") {\n this.lastPage = null;\n return;\n }\n\n // If autocollect is off, only collect if explicitly enabled via meta/body\n if (!this.config.autocollect && metaCollect !== \"true\" && bodyCollect !== \"true\") {\n this.lastPage = null;\n return;\n }\n\n this.trackPageView({}, true);\n };\n\n // visibilitychange\n const onVisibility = () => {\n if (document.visibilityState === \"visible\") handlePageView();\n };\n document.addEventListener(\"visibilitychange\", onVisibility);\n\n // pushState\n const origPush = history.pushState.bind(history);\n history.pushState = (...args) => {\n origPush(...args);\n requestAnimationFrame(() => {\n handlePageView();\n });\n };\n\n // popstate\n window.addEventListener(\"popstate\", handlePageView);\n\n // hashchange\n window.addEventListener(\"hashchange\", handlePageView);\n\n // click autocapture\n const onClick: EventListener = (ev: Event) => {\n const clickEvent = ev as MouseEvent;\n if (clickEvent.type === \"auxclick\" && clickEvent.button !== 1) return;\n\n const target = clickEvent.target as Element | null;\n if (!target) return;\n\n // Check if inside <a> or <button>\n const insideInteractive = !!target.closest(\"a, button\");\n\n let el: Element | null = target;\n let depth = 0;\n\n while (el) {\n const eventName = el.getAttribute(\"data-s-event\") || el.getAttribute(\"data-s:event\");\n if (eventName) {\n const propsAttr = el.getAttribute(\"data-s-event-props\") || el.getAttribute(\"data-s:event-props\");\n const props = propsAttr ? parseProps(propsAttr) : undefined;\n const path = el.getAttribute(\"data-s-event-path\") || el.getAttribute(\"data-s:event-path\") || undefined;\n\n if ((path && !shouldTrackPath(path, this.config)) || !shouldTrackPath(resolvePath(undefined, this.config.hashRouting), this.config)) {\n return;\n }\n\n this.event(eventName, path ?? props, props);\n return;\n }\n\n el = el.parentElement;\n depth++;\n\n // If not in <a>/<button>, stop after 3 levels\n if (!insideInteractive && depth >= 3) break;\n }\n };\n\n document.addEventListener(\"click\", onClick);\n\n // Fire initial pageview if already visible\n if (document.visibilityState === \"visible\") handlePageView();\n }\n}\n\nexport const configure = (userConfig: AnalyticsConfig = {}) => {\n AnalyticsTracker.getInstance(userConfig);\n};\n\nexport const event = async (eventName: string, pathOrProps?: string | BaseProps, props?: BaseProps) => {\n const instance = AnalyticsTracker.getInstance();\n await instance.event(eventName, pathOrProps, props);\n};\n\nexport const view = async (pathOrProps?: string | BaseProps, props?: BaseProps) => {\n const instance = AnalyticsTracker.getInstance();\n await instance.view(pathOrProps, props);\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|