onedollarstats 0.0.20 → 0.0.22

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
@@ -119,7 +119,7 @@ event("Purchase", "/product", { amount: 1, color: "green" });
119
119
  - `pathOrProps` – Optional, **string** represents the path, **object** represents custom properties.
120
120
  - `props` – Optional, properties if the second argument is a path string.
121
121
 
122
- ## Expo / React Native
122
+ ## Expo
123
123
 
124
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.
125
125
 
package/dist/expo.js CHANGED
@@ -142,7 +142,8 @@ function send(eventName, path, config, props) {
142
142
  if (config.devmode) devLog(eventName, url, props);
143
143
  const body = JSON.stringify({
144
144
  u: url,
145
- e: [{ t: eventName, ...props && { p: props } }]
145
+ e: [{ t: eventName, ...props && { p: props } }],
146
+ debug: config.devmode
146
147
  });
147
148
  if (Platform.OS === "web") {
148
149
  sendWeb(config.collectorUrl, body);
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 });\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 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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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onedollarstats",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
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",