onedollarstats 0.0.23 → 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 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 on app foreground, supports dynamic-route templates (`/profile/[id]` instead of `/profile/abc123`), and sends events natively on iOS/Android and via image beacon + `sendBeacon` on web.
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
- All other options (`hostname`, `collectorUrl`, `devmode`, `autocollect`, `excludePages`, `includePages`) behave the same as in the web tracker above.
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
- devmode: config.devmode ?? false,
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 /^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(hostname) && (protocol === "http:" || protocol === "https:");
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 (merged.devmode && isWebLocalhost()) {
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)) return;
81
- if (lastPathRef.current === trackedPath) return;
82
- lastPathRef.current = trackedPath;
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 !== "active") return;
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 = lastPathRef.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
- if (Platform.OS !== "web") return false;
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 = `https://${config.hostname}${path}`;
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": "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",
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
@@ -1,5 +1,26 @@
1
+ // src/utils/environment.ts
2
+ var getEnvironment = () => {
3
+ const isDesktop = /Electron\//.test(navigator.userAgent) || typeof window.__TAURI_INTERNALS__?.invoke === "function" || typeof window.__TAURI_IPC__ === "function" || /^(tauri|wails):$/.test(location.protocol) || /^(tauri|wails)\.localhost$/.test(location.hostname) || typeof window.NL_VERSION === "string" || typeof window.nw?.App === "object";
4
+ return {
5
+ // Desktop apps are served from localhost/file:// in production too, so they are never "localhost"
6
+ isLocalhost: !isDesktop && (/^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(location.hostname) && //TODO revisit
7
+ (location.protocol === "http:" || location.protocol === "https:") || location.protocol === "file:" && !navigator.userAgent.toLowerCase().includes("electron")),
8
+ isDesktop
9
+ };
10
+ };
11
+ var isClient = () => {
12
+ try {
13
+ if (typeof window === "undefined" || typeof document === "undefined") return false;
14
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
15
+ if (/^Node\.js\/|jsdom/i.test(ua)) return false;
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ };
21
+
1
22
  // src/utils/bot.ts
2
- var BOT_UA_PATTERN = /Google-InspectionTool|Mediapartners-Google|Sogou|ChatGPT-User|anthropic-ai|facebookexternalhit|WhatsApp|Snapchat|HeadlessChrome|PhantomJS|Selenium|Puppeteer|curl\/|Wget\/|python-requests|python-urllib|axios\/|Go-http-client|Java\/|libwww-perl|Apache-HttpClient|okhttp|Scrapy|bot|crawl|spider|slurp|fetch|archiver/i;
23
+ var BOT_UA_PATTERN = /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;
3
24
  var AUTOMATION_GLOBALS = [
4
25
  // Selenium
5
26
  "__selenium_unwrapped",
@@ -16,7 +37,6 @@ var AUTOMATION_GLOBALS = [
16
37
  // PhantomJS
17
38
  "callPhantom",
18
39
  "_phantom",
19
- "phantom",
20
40
  // Nightmare.js
21
41
  "__nightmare",
22
42
  // Playwright (injects page.exposeFunction bindings)
@@ -26,14 +46,17 @@ var AUTOMATION_GLOBALS = [
26
46
  "__casper",
27
47
  // TestCafe
28
48
  "__testcafe",
29
- // WebDriver (generic)
30
- "webdriver",
49
+ // Cypress
50
+ "Cypress",
51
+ // Chrome automation
31
52
  "domAutomation",
32
53
  "domAutomationController"
33
54
  ];
34
55
  function detectBot() {
35
56
  const signals = collectBotSignals();
36
- const isBot = signals.userAgentBot || signals.webdriver || signals.headless || signals.automationGlobals.length > 0 || signals.liesDetected > 2 || signals.liesDetected > 0 && signals.hasProxy;
57
+ const { isDesktop } = getEnvironment();
58
+ if (isDesktop) signals.userAgentBot = signals.headless = false;
59
+ const isBot = signals.userAgentBot || signals.webdriver || signals.headless || signals.automationGlobals.length > 0;
37
60
  let botKind = "human";
38
61
  if (isBot) {
39
62
  if (signals.headless) {
@@ -46,15 +69,19 @@ function detectBot() {
46
69
  }
47
70
  return { isBot, botKind, signals };
48
71
  }
72
+ function shouldBlock(devmode) {
73
+ if (getEnvironment().isLocalhost && !devmode) return true;
74
+ const { isBot, botKind, signals } = detectBot();
75
+ if (isBot && devmode) console.log(`[onedollarstats]
76
+ Event blocked as bot (${botKind}): ${JSON.stringify(signals)}`);
77
+ return isBot;
78
+ }
49
79
  function collectBotSignals() {
50
80
  return {
51
81
  userAgentBot: detectUserAgentBot(),
52
82
  webdriver: detectWebdriver(),
53
83
  headless: detectHeadless(),
54
- automationGlobals: detectAutomationGlobals(),
55
- ...detectLies(),
56
- missingLanguages: detectMissingLanguages(),
57
- missingPlugins: detectMissingPlugins()
84
+ automationGlobals: detectAutomationGlobals()
58
85
  };
59
86
  }
60
87
  function detectUserAgentBot() {
@@ -66,104 +93,18 @@ function detectWebdriver() {
66
93
  return !!navigator.webdriver;
67
94
  }
68
95
  function detectHeadless() {
69
- const w = window;
70
- const n = navigator;
71
- if (/Chrome/.test(n.userAgent) && !w.chrome) return true;
72
- if (/HeadlessChrome/.test(n.userAgent)) return true;
73
- try {
74
- if (Notification.permission === "denied" && n.permissions) {
75
- if ((!n.plugins || n.plugins.length === 0) && !/Mobile|Android/i.test(n.userAgent)) {
76
- return true;
77
- }
78
- }
79
- } catch {
80
- }
81
- return false;
96
+ return /HeadlessChrome/.test(navigator.userAgent);
82
97
  }
83
98
  function detectAutomationGlobals() {
84
99
  const w = window;
85
100
  return AUTOMATION_GLOBALS.filter((key) => {
86
101
  try {
87
- return key in w && w[key] !== void 0;
102
+ return Object.prototype.hasOwnProperty.call(w, key) && w[key] !== void 0;
88
103
  } catch {
89
104
  return false;
90
105
  }
91
106
  });
92
107
  }
93
- function detectLies() {
94
- let liesDetected = 0;
95
- let hasProxy = false;
96
- const apisToTest = [
97
- [Navigator.prototype, "userAgent"],
98
- [Navigator.prototype, "languages"],
99
- [Navigator.prototype, "platform"],
100
- [Navigator.prototype, "hardwareConcurrency"],
101
- [Navigator.prototype, "webdriver"],
102
- [HTMLCanvasElement.prototype, "toDataURL"],
103
- [CanvasRenderingContext2D.prototype, "fillText"],
104
- [Date.prototype, "getTimezoneOffset"]
105
- ];
106
- for (const [proto, prop] of apisToTest) {
107
- try {
108
- const d = Object.getOwnPropertyDescriptor(proto, prop);
109
- if (!d) continue;
110
- const fn = d.value ?? d.get;
111
- if (typeof fn !== "function") continue;
112
- const str = Function.prototype.toString.call(fn);
113
- if (!isNativeToString(str)) liesDetected++;
114
- if (d.value && fn.toString !== Function.prototype.toString) {
115
- try {
116
- const native = Function.prototype.toString.call(fn);
117
- const custom = fn.toString();
118
- if (native !== custom) {
119
- liesDetected++;
120
- hasProxy = true;
121
- }
122
- } catch {
123
- liesDetected++;
124
- hasProxy = true;
125
- }
126
- }
127
- } catch {
128
- }
129
- }
130
- try {
131
- const s = Function.prototype.toString.call(Function.prototype.toString);
132
- if (!isNativeToString(s)) liesDetected++;
133
- } catch {
134
- }
135
- return { liesDetected, hasProxy };
136
- }
137
- function detectMissingLanguages() {
138
- const langs = navigator.languages;
139
- return !langs || langs.length === 0;
140
- }
141
- function detectMissingPlugins() {
142
- if (/Mobile|Android/i.test(navigator.userAgent)) return false;
143
- return !navigator.plugins || navigator.plugins.length === 0;
144
- }
145
- function isNativeToString(str) {
146
- return /^function\s[^{]*\{\s*\[native code\]\s*\}$/.test(str) || str === "function () { [native code] }" || /^\(\)\s*=>\s*\{\s*\[native code\]\s*\}$/.test(str);
147
- }
148
-
149
- // src/utils/environment.ts
150
- var getEnvironment = () => ({
151
- isLocalhost: /^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(location.hostname) && //TODO revisit
152
- (location.protocol === "http:" || location.protocol === "https:") || location.protocol === "file:" && !navigator.userAgent.toLowerCase().includes("electron"),
153
- isHeadlessBrowser: Boolean(
154
- window.navigator.webdriver || "_phantom" in window && window._phantom || "__nightmare" in window && window.__nightmare || "Cypress" in window && window.Cypress
155
- )
156
- });
157
- var isClient = () => {
158
- try {
159
- if (typeof window === "undefined" || typeof document === "undefined") return false;
160
- const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
161
- if (/node|jsdom/i.test(ua)) return false;
162
- return true;
163
- } catch {
164
- return false;
165
- }
166
- };
167
108
 
168
109
  // src/utils/merge-config.ts
169
110
  var defaultConfig = {
@@ -176,8 +117,8 @@ var defaultConfig = {
176
117
  includePages: []
177
118
  };
178
119
  var mergeConfig = (userConfig = {}) => {
179
- const { isLocalhost } = getEnvironment();
180
- const devmode = !isLocalhost ? false : userConfig.devmode ?? !!userConfig.trackLocalhostAs;
120
+ const { isLocalhost, isDesktop } = getEnvironment();
121
+ const devmode = !isLocalhost && !isDesktop ? false : userConfig.devmode ?? !!userConfig.trackLocalhostAs;
181
122
  let hostname;
182
123
  if (userConfig.hostname) {
183
124
  const trimmed = userConfig.hostname.trim();
@@ -232,7 +173,7 @@ var parseProps = (propsString) => {
232
173
  };
233
174
 
234
175
  // src/utils/resolve-path.ts
235
- var resolvePath = (pathOrProps) => {
176
+ var resolvePath = (pathOrProps, hashRouting = false) => {
236
177
  if (pathOrProps) return pathOrProps;
237
178
  const sources = [
238
179
  { value: document.body?.getAttribute("data-s-path"), name: "data-s-path" },
@@ -243,7 +184,9 @@ var resolvePath = (pathOrProps) => {
243
184
  if (existing.length > 1) {
244
185
  console.warn("[onedollarstats] Multiple path sources found. Using priority order:", existing.map(({ name }) => name).join(" > "));
245
186
  }
246
- return existing[0]?.value ?? location.pathname;
187
+ const hashPath = hashRouting && location.hash.replace(/^#!?\/?/, "/").split("?")[0];
188
+ const urlPath = location.protocol === "file:" ? "/" : location.pathname;
189
+ return existing[0]?.value ?? (hashPath || urlPath);
247
190
  };
248
191
 
249
192
  // src/utils/should-track.ts
@@ -305,19 +248,17 @@ OneDollarStats connected! Tracking localhost as ${this.config.hostname}`);
305
248
  // Handles localhost replacement, referrer, UTM parameters, and debug mode.
306
249
  // Uses img beacon then `navigator.sendBeacon` if available, otherwise falls back to `fetch`.
307
250
  async send(data) {
308
- const { isLocalhost, isHeadlessBrowser } = getEnvironment();
309
- if (isLocalhost && !this.config.devmode || isHeadlessBrowser) return;
310
- const { isBot, botKind } = detectBot();
311
- if (isBot && botKind !== "human") return;
251
+ if (shouldBlock(this.config.devmode)) return;
312
252
  const urlToSend = new URL(this.config.hostname ? `https://${this.config.hostname}${location.pathname}` : location.href);
313
253
  urlToSend.search = "";
254
+ urlToSend.hash = "";
314
255
  if (data.path) urlToSend.pathname = data.path;
315
256
  const cleanUrl = urlToSend.href.replace(/\/$/, "");
316
257
  let referrer = data.referrer;
317
258
  try {
318
259
  if (!referrer && document.referrer && document.referrer !== "null") {
319
260
  const referrerURL = new URL(document.referrer);
320
- if (referrerURL.hostname !== urlToSend.hostname) referrer = referrerURL.href;
261
+ if (/^https?:$/.test(referrerURL.protocol) && referrerURL.hostname !== urlToSend.hostname && referrerURL.hostname !== location.hostname) referrer = referrerURL.href;
321
262
  }
322
263
  } catch {
323
264
  }
@@ -372,7 +313,7 @@ UTM: ${data.utm}`;
372
313
  // Prevents duplicate pageviews and respects include/exclude page rules. Automatically parses UTM parameters from URL.
373
314
  trackPageView({ path, props }, checkBlock = false) {
374
315
  if (!isClient()) return;
375
- const viewPath = resolvePath(path);
316
+ const viewPath = resolvePath(path, this.config.hashRouting);
376
317
  const collectedProps = {};
377
318
  const elements = document.querySelectorAll("[data-s\\:view-props], [data-s-view-props]");
378
319
  for (const el of Array.from(elements)) {
@@ -389,8 +330,11 @@ UTM: ${data.utm}`;
389
330
  Object.assign(collectedProps, props);
390
331
  }
391
332
  const viewProps = Object.keys(collectedProps).length > 0 ? collectedProps : void 0;
392
- if (!this.config.hashRouting && this.lastPage === viewPath) return;
393
- if (checkBlock && !shouldTrackPath(viewPath, this.config)) return;
333
+ if (checkBlock && !shouldTrackPath(viewPath, this.config)) {
334
+ this.lastPage = null;
335
+ return;
336
+ }
337
+ if (this.lastPage === viewPath) return;
394
338
  this.lastPage = viewPath;
395
339
  const utm = parseUtmParams(new URLSearchParams(location.search));
396
340
  this.send({ type: "PageView", path: viewPath, props: viewProps, utm });
@@ -405,9 +349,7 @@ UTM: ${data.utm}`;
405
349
  */
406
350
  async event(eventName, pathOrProps, rawProps) {
407
351
  if (!isClient()) return;
408
- const { isLocalhost, isHeadlessBrowser } = getEnvironment();
409
- if (isLocalhost && !this.config.devmode || isHeadlessBrowser) return;
410
- const path = resolvePath(typeof pathOrProps === "string" ? pathOrProps : void 0);
352
+ const path = resolvePath(typeof pathOrProps === "string" ? pathOrProps : void 0, this.config.hashRouting);
411
353
  const props = typeof pathOrProps === "object" ? pathOrProps : rawProps;
412
354
  this.send({ type: eventName, path, props });
413
355
  }
@@ -481,7 +423,7 @@ UTM: ${data.utm}`;
481
423
  const propsAttr = el.getAttribute("data-s-event-props") || el.getAttribute("data-s:event-props");
482
424
  const props = propsAttr ? parseProps(propsAttr) : void 0;
483
425
  const path = el.getAttribute("data-s-event-path") || el.getAttribute("data-s:event-path") || void 0;
484
- if (path && !shouldTrackPath(path, this.config) || !shouldTrackPath(location.pathname, this.config)) {
426
+ if (path && !shouldTrackPath(path, this.config) || !shouldTrackPath(resolvePath(void 0, this.config.hashRouting), this.config)) {
485
427
  return;
486
428
  }
487
429
  this.event(eventName, path ?? props, props);
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/utils/bot.ts", "../src/utils/environment.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": ["/**\n * Bot & crawler detection module.\n *\n * Detects:\n * - Known search engine crawlers (Googlebot, Bingbot, Yandex, Baidu, etc.)\n * - Social media crawlers (Facebook, Twitter, LinkedIn, etc.)\n * - Headless browsers (Puppeteer, Playwright, Selenium, PhantomJS)\n * - General automation tools via navigator.webdriver and injected globals\n * - API tampering / lie detection (prototype spoofing, proxy wrapping)\n */\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type BotKind =\n | 'search_engine' // Googlebot, Bingbot, Yandex, Baidu, DuckDuckBot, etc.\n | 'social_crawler' // Facebook, Twitter/X, LinkedIn, Slack, Discord, etc.\n | 'headless' // Headless Chrome/Firefox, PhantomJS\n | 'automation' // Selenium, Puppeteer, Playwright (non-headless)\n | 'library' // curl, wget, python-requests, node-fetch, etc.\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 /** Number of API lies (toString/proxy tampering) detected. */\n liesDetected: number\n /** Proxy wrapping detected on native functions. */\n hasProxy: boolean\n /** navigator.languages is empty or missing. */\n missingLanguages: boolean\n /** navigator.plugins is empty (non-mobile). */\n missingPlugins: boolean\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.\nconst BOT_UA_PATTERN =\n /Google-InspectionTool|Mediapartners-Google|Sogou|ChatGPT-User|anthropic-ai|facebookexternalhit|WhatsApp|Snapchat|HeadlessChrome|PhantomJS|Selenium|Puppeteer|curl\\/|Wget\\/|python-requests|python-urllib|axios\\/|Go-http-client|Java\\/|libwww-perl|Apache-HttpClient|okhttp|Scrapy|bot|crawl|spider|slurp|fetch|archiver/i\n\n// ---------------------------------------------------------------------------\n// Automation globals injected by common frameworks\n// ---------------------------------------------------------------------------\n\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 '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 // WebDriver (generic)\n 'webdriver',\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\n const isBot =\n signals.userAgentBot ||\n signals.webdriver ||\n signals.headless ||\n signals.automationGlobals.length > 0 ||\n signals.liesDetected > 2 ||\n (signals.liesDetected > 0 && signals.hasProxy)\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\n// ---------------------------------------------------------------------------\n// Signal collectors\n// ---------------------------------------------------------------------------\n\nfunction collectBotSignals(): BotSignals {\n return {\n userAgentBot: detectUserAgentBot(),\n webdriver: detectWebdriver(),\n headless: detectHeadless(),\n automationGlobals: detectAutomationGlobals(),\n ...detectLies(),\n missingLanguages: detectMissingLanguages(),\n missingPlugins: detectMissingPlugins(),\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/** Headless browser indicators. */\nfunction detectHeadless(): boolean {\n const w = window as any\n const n = navigator as any\n\n // Chrome-specific: headless mode omits the chrome runtime object\n if (/Chrome/.test(n.userAgent) && !w.chrome) return true\n\n // HeadlessChrome in UA\n if (/HeadlessChrome/.test(n.userAgent)) return true\n\n // Notification permission is always \"denied\" in headless Chrome\n // (in headed mode it defaults to \"default\")\n try {\n if (Notification.permission === 'denied' && n.permissions) {\n // Double-check: headless also lacks plugins\n if ((!n.plugins || n.plugins.length === 0) && !/Mobile|Android/i.test(n.userAgent)) {\n return true\n }\n }\n } catch { /* permissions API unavailable */ }\n\n return false\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 { return key in w && w[key] !== undefined }\n catch { return false }\n }) as string[]\n}\n\n/**\n * Lie detection \u2014 detect API tampering (proxies, toString spoofing).\n * Extracted from the fingerprint lies module; self-contained here.\n */\nfunction detectLies(): { liesDetected: number; hasProxy: boolean } {\n let liesDetected = 0\n let hasProxy = false\n\n const apisToTest: Array<[object, string]> = [\n [Navigator.prototype, 'userAgent'],\n [Navigator.prototype, 'languages'],\n [Navigator.prototype, 'platform'],\n [Navigator.prototype, 'hardwareConcurrency'],\n [Navigator.prototype, 'webdriver'],\n [HTMLCanvasElement.prototype, 'toDataURL'],\n [CanvasRenderingContext2D.prototype, 'fillText'],\n [Date.prototype, 'getTimezoneOffset'],\n ]\n\n for (const [proto, prop] of apisToTest) {\n try {\n const d = Object.getOwnPropertyDescriptor(proto, prop)\n if (!d) continue\n const fn = d.value ?? d.get\n if (typeof fn !== 'function') continue\n\n // toString integrity for methods and accessor getters\n const str = Function.prototype.toString.call(fn)\n if (!isNativeToString(str)) liesDetected++\n\n // Proxy detection \u2014 only for value-bound methods (not getters)\n if (d.value && fn.toString !== Function.prototype.toString) {\n try {\n const native = Function.prototype.toString.call(fn)\n const custom = fn.toString()\n if (native !== custom) { liesDetected++; hasProxy = true }\n } catch { liesDetected++; hasProxy = true }\n }\n } catch { /* skip inaccessible */ }\n }\n\n // Meta-test: toString integrity\n try {\n const s = Function.prototype.toString.call(Function.prototype.toString)\n if (!isNativeToString(s)) liesDetected++\n } catch { /* skip */ }\n\n return { liesDetected, hasProxy }\n}\n\n/** Missing navigator.languages is a strong headless indicator. */\nfunction detectMissingLanguages(): boolean {\n const langs = navigator.languages\n return !langs || langs.length === 0\n}\n\n/** Missing plugins on desktop is a headless indicator. */\nfunction detectMissingPlugins(): boolean {\n if (/Mobile|Android/i.test(navigator.userAgent)) return false\n return !navigator.plugins || navigator.plugins.length === 0\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction isNativeToString(str: string): boolean {\n return /^function\\s[^{]*\\{\\s*\\[native code\\]\\s*\\}$/.test(str) ||\n str === 'function () { [native code] }' ||\n /^\\(\\)\\s*=>\\s*\\{\\s*\\[native code\\]\\s*\\}$/.test(str)\n}\n", "export const getEnvironment = (): {\n isLocalhost: boolean;\n isHeadlessBrowser: boolean;\n} => ({\n isLocalhost:\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 isHeadlessBrowser: Boolean(\n window.navigator.webdriver ||\n (\"_phantom\" in window && window._phantom) ||\n (\"__nightmare\" in window && window.__nightmare) ||\n (\"Cypress\" in window && window.Cypress)\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 if (/node|jsdom/i.test(ua)) return false;\n return true;\n } catch {\n return false;\n }\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 } = getEnvironment();\n\n const devmode = !isLocalhost ? 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): 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 // Return first available value, fallback to location.pathname\n return existing[0]?.value ?? location.pathname;\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 { detectBot } 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 const { isLocalhost, isHeadlessBrowser } = getEnvironment();\n if ((isLocalhost && !this.config.devmode) || isHeadlessBrowser) return;\n\n const { isBot, botKind } = detectBot();\n\n if (isBot && botKind !== \"human\") 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 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 if (referrerURL.hostname !== urlToSend.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);\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.config.hashRouting && 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 { isLocalhost, isHeadlessBrowser } = getEnvironment();\n if ((isLocalhost && !this.config.devmode) || isHeadlessBrowser) return;\n\n const path = resolvePath(typeof pathOrProps === \"string\" ? pathOrProps : undefined);\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(location.pathname, 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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",
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 // 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onedollarstats",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "A lightweight, zero-dependency analytics tracker for frontend apps",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",