@probie-dev/react-native 0.1.0

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.
Files changed (56) hide show
  1. package/README.md +221 -0
  2. package/dist/appstate.d.ts +1 -0
  3. package/dist/appstate.js +31 -0
  4. package/dist/buffer.d.ts +13 -0
  5. package/dist/buffer.js +27 -0
  6. package/dist/capture/forms.d.ts +16 -0
  7. package/dist/capture/forms.js +33 -0
  8. package/dist/capture/screen.d.ts +7 -0
  9. package/dist/capture/screen.js +30 -0
  10. package/dist/capture/tap.d.ts +11 -0
  11. package/dist/capture/tap.js +37 -0
  12. package/dist/client.d.ts +33 -0
  13. package/dist/client.js +165 -0
  14. package/dist/config.d.ts +18 -0
  15. package/dist/config.js +64 -0
  16. package/dist/deviceContext.d.ts +2 -0
  17. package/dist/deviceContext.js +43 -0
  18. package/dist/element.d.ts +3 -0
  19. package/dist/element.js +26 -0
  20. package/dist/hash.d.ts +1 -0
  21. package/dist/hash.js +6 -0
  22. package/dist/identity.d.ts +6 -0
  23. package/dist/identity.js +18 -0
  24. package/dist/ids.d.ts +1 -0
  25. package/dist/ids.js +13 -0
  26. package/dist/index.d.ts +8 -0
  27. package/dist/index.js +7 -0
  28. package/dist/interceptors/errors.d.ts +3 -0
  29. package/dist/interceptors/errors.js +59 -0
  30. package/dist/interceptors/network.d.ts +3 -0
  31. package/dist/interceptors/network.js +47 -0
  32. package/dist/nav/expoRouter.d.ts +1 -0
  33. package/dist/nav/expoRouter.js +17 -0
  34. package/dist/nav/reactNavigation.d.ts +6 -0
  35. package/dist/nav/reactNavigation.js +25 -0
  36. package/dist/react/ProbiePressable.d.ts +6 -0
  37. package/dist/react/ProbiePressable.js +21 -0
  38. package/dist/react/ProbieProvider.d.ts +7 -0
  39. package/dist/react/ProbieProvider.js +11 -0
  40. package/dist/react/hooks.d.ts +2 -0
  41. package/dist/react/hooks.js +4 -0
  42. package/dist/react/useProbieForm.d.ts +6 -0
  43. package/dist/react/useProbieForm.js +12 -0
  44. package/dist/session.d.ts +7 -0
  45. package/dist/session.js +36 -0
  46. package/dist/singleton.d.ts +10 -0
  47. package/dist/singleton.js +30 -0
  48. package/dist/transport.d.ts +9 -0
  49. package/dist/transport.js +67 -0
  50. package/dist/types.d.ts +66 -0
  51. package/dist/types.js +1 -0
  52. package/dist/url.d.ts +1 -0
  53. package/dist/url.js +21 -0
  54. package/dist/version.d.ts +2 -0
  55. package/dist/version.js +2 -0
  56. package/package.json +64 -0
package/dist/config.js ADDED
@@ -0,0 +1,64 @@
1
+ function memoryStorage() {
2
+ const map = new Map();
3
+ return {
4
+ getItem: async (k) => (map.has(k) ? map.get(k) : null),
5
+ setItem: async (k, v) => {
6
+ map.set(k, v);
7
+ },
8
+ removeItem: async (k) => {
9
+ map.delete(k);
10
+ },
11
+ };
12
+ }
13
+ function resolveStorage(injected) {
14
+ if (injected)
15
+ return injected;
16
+ try {
17
+ const mod = require('@react-native-async-storage/async-storage');
18
+ const store = mod.default ?? mod;
19
+ if (store && typeof store.getItem === 'function')
20
+ return store;
21
+ }
22
+ catch {
23
+ }
24
+ return memoryStorage();
25
+ }
26
+ function resolveFetch(injected) {
27
+ if (injected)
28
+ return injected;
29
+ const g = globalThis;
30
+ if (typeof g.fetch === 'function')
31
+ return (url, init) => g.fetch(url, init);
32
+ return async () => undefined;
33
+ }
34
+ function clampRate(raw) {
35
+ const n = raw == null ? 1 : raw;
36
+ if (typeof n !== 'number' || Number.isNaN(n))
37
+ return 1;
38
+ return Math.min(1, Math.max(0, n));
39
+ }
40
+ export function normalizeConfig(config) {
41
+ if (!config || typeof config.token !== 'string' || config.token.length === 0) {
42
+ throw new Error('probie: config.token is required');
43
+ }
44
+ if (typeof config.apiBase !== 'string' || config.apiBase.length === 0) {
45
+ throw new Error('probie: config.apiBase is required');
46
+ }
47
+ const apiBase = config.apiBase.replace(/\/+$/, '');
48
+ return {
49
+ token: config.token,
50
+ apiBase,
51
+ endpoint: apiBase + '/api/widget/events',
52
+ ownPrefix: apiBase + '/api/widget',
53
+ sampleRate: clampRate(config.sampleRate),
54
+ captureQuery: config.captureQuery === true,
55
+ captureErrors: config.captureErrors !== false,
56
+ captureNetwork: config.captureNetwork !== false,
57
+ captureUnhandledRejections: config.captureUnhandledRejections !== false,
58
+ idleTimeoutMs: config.idleTimeoutMs ?? null,
59
+ flushIntervalMs: config.flushIntervalMs && config.flushIntervalMs > 0 ? config.flushIntervalMs : 12000,
60
+ appVersion: config.appVersion,
61
+ storage: resolveStorage(config.storage),
62
+ fetchImpl: resolveFetch(config.fetchImpl),
63
+ };
64
+ }
@@ -0,0 +1,2 @@
1
+ import type { DeviceContext } from './types.js';
2
+ export declare function buildDeviceContext(appVersionOverride?: string): DeviceContext;
@@ -0,0 +1,43 @@
1
+ import { SDK_NAME, SDK_VERSION } from './version.js';
2
+ export function buildDeviceContext(appVersionOverride) {
3
+ const ctx = {
4
+ platform: null,
5
+ os_version: null,
6
+ app_version: appVersionOverride ?? null,
7
+ build: null,
8
+ device_model: null,
9
+ sdk_name: SDK_NAME,
10
+ sdk_version: SDK_VERSION,
11
+ anon_id: null,
12
+ };
13
+ try {
14
+ const { Platform } = require('react-native');
15
+ ctx.platform = Platform?.OS ?? null;
16
+ ctx.os_version = Platform?.Version != null ? String(Platform.Version) : null;
17
+ }
18
+ catch {
19
+ }
20
+ try {
21
+ const App = require('expo-application');
22
+ if (ctx.app_version == null)
23
+ ctx.app_version = App.nativeApplicationVersion ?? null;
24
+ ctx.build = App.nativeBuildVersion ?? null;
25
+ }
26
+ catch {
27
+ }
28
+ try {
29
+ const Device = require('expo-device');
30
+ ctx.device_model = Device.modelName ?? null;
31
+ }
32
+ catch {
33
+ }
34
+ if (ctx.app_version == null) {
35
+ try {
36
+ const Constants = require('expo-constants').default ?? require('expo-constants');
37
+ ctx.app_version = Constants?.expoConfig?.version ?? null;
38
+ }
39
+ catch {
40
+ }
41
+ }
42
+ return ctx;
43
+ }
@@ -0,0 +1,3 @@
1
+ import type { ElementInfo, ElementSource } from './types.js';
2
+ export declare function elementInfoFrom(source: ElementSource | ElementInfo | null | undefined): ElementInfo | null;
3
+ export declare function elementKey(source: ElementSource | ElementInfo | null | undefined): string;
@@ -0,0 +1,26 @@
1
+ import { hashString } from './hash.js';
2
+ function isElementInfo(x) {
3
+ return typeof x.path === 'string' && 'text_hash' in x;
4
+ }
5
+ export function elementInfoFrom(source) {
6
+ if (!source)
7
+ return null;
8
+ if (isElementInfo(source))
9
+ return source;
10
+ const role = source.accessibilityRole ?? null;
11
+ const testid = source.testID ?? null;
12
+ const label = String(source.accessibilityLabel ?? source.text ?? '').trim().slice(0, 80);
13
+ const describe = `${role ?? 'view'}${testid ? '#' + testid : ''}`;
14
+ return {
15
+ path: source.path ?? describe,
16
+ role,
17
+ name: source.name ?? null,
18
+ type: source.type ?? null,
19
+ testid,
20
+ text_hash: label ? hashString(label) : null,
21
+ };
22
+ }
23
+ export function elementKey(source) {
24
+ const info = elementInfoFrom(source);
25
+ return info ? info.path : 'view';
26
+ }
package/dist/hash.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function hashString(s: string): string;
package/dist/hash.js ADDED
@@ -0,0 +1,6 @@
1
+ export function hashString(s) {
2
+ let h = 5381;
3
+ for (let i = 0; i < s.length; i++)
4
+ h = ((h << 5) + h + s.charCodeAt(i)) | 0;
5
+ return (h >>> 0).toString(36);
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { AsyncStorageLike } from './types.js';
2
+ export interface Identity {
3
+ getAnonId(): string | null;
4
+ readonly ready: Promise<void>;
5
+ }
6
+ export declare function createIdentity(storage: AsyncStorageLike): Identity;
@@ -0,0 +1,18 @@
1
+ import { genId } from './ids.js';
2
+ const DID_KEY = 'probie_did';
3
+ export function createIdentity(storage) {
4
+ let anonId = null;
5
+ const ready = (async () => {
6
+ try {
7
+ let did = await storage.getItem(DID_KEY);
8
+ if (!did) {
9
+ did = genId();
10
+ await storage.setItem(DID_KEY, did);
11
+ }
12
+ anonId = did;
13
+ }
14
+ catch {
15
+ }
16
+ })();
17
+ return { getAnonId: () => anonId, ready };
18
+ }
package/dist/ids.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function genId(): string;
package/dist/ids.js ADDED
@@ -0,0 +1,13 @@
1
+ export function genId() {
2
+ try {
3
+ const crypto = require('expo-crypto');
4
+ if (crypto && typeof crypto.randomUUID === 'function') {
5
+ const id = crypto.randomUUID();
6
+ if (id)
7
+ return String(id);
8
+ }
9
+ }
10
+ catch {
11
+ }
12
+ return 'pb-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
13
+ }
@@ -0,0 +1,8 @@
1
+ export type { ProbieClient, ProbieConfig, ProbieEvent, EventType, ElementInfo, ElementSource, DeviceContext } from './types.js';
2
+ export { init, getClient, identify, reset, trackScreen, track, flush, __teardown } from './singleton.js';
3
+ export { ProbieProvider } from './react/ProbieProvider.js';
4
+ export { ProbiePressable } from './react/ProbiePressable.js';
5
+ export { useProbieForm } from './react/useProbieForm.js';
6
+ export { useProbieClient } from './react/hooks.js';
7
+ export { useProbieExpoRouter } from './nav/expoRouter.js';
8
+ export { probieNavigationContainerProps } from './nav/reactNavigation.js';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { init, getClient, identify, reset, trackScreen, track, flush, __teardown } from './singleton.js';
2
+ export { ProbieProvider } from './react/ProbieProvider.js';
3
+ export { ProbiePressable } from './react/ProbiePressable.js';
4
+ export { useProbieForm } from './react/useProbieForm.js';
5
+ export { useProbieClient } from './react/hooks.js';
6
+ export { useProbieExpoRouter } from './nav/expoRouter.js';
7
+ export { probieNavigationContainerProps } from './nav/reactNavigation.js';
@@ -0,0 +1,3 @@
1
+ type Emit = (type: 'js_error', payload: Record<string, unknown>) => void;
2
+ export declare function installErrors(emit: Emit, captureUnhandledRejections: boolean): () => void;
3
+ export {};
@@ -0,0 +1,59 @@
1
+ function messageOf(err) {
2
+ if (err && typeof err === 'object' && 'message' in err && err.message) {
3
+ return String(err.message).slice(0, 500);
4
+ }
5
+ return String(err).slice(0, 500);
6
+ }
7
+ function stackOf(err) {
8
+ if (err && typeof err === 'object' && 'stack' in err && err.stack) {
9
+ return String(err.stack).slice(0, 2000);
10
+ }
11
+ return '';
12
+ }
13
+ export function installErrors(emit, captureUnhandledRejections) {
14
+ const cleanups = [];
15
+ const EU = globalThis.ErrorUtils;
16
+ if (EU && typeof EU.getGlobalHandler === 'function' && typeof EU.setGlobalHandler === 'function') {
17
+ const prev = EU.getGlobalHandler();
18
+ EU.setGlobalHandler((error, isFatal) => {
19
+ try {
20
+ emit('js_error', { message: messageOf(error), stack: stackOf(error), fatal: !!isFatal });
21
+ }
22
+ catch {
23
+ }
24
+ if (typeof prev === 'function')
25
+ prev(error, isFatal);
26
+ });
27
+ cleanups.push(() => {
28
+ try {
29
+ EU.setGlobalHandler(prev);
30
+ }
31
+ catch {
32
+ }
33
+ });
34
+ }
35
+ if (captureUnhandledRejections) {
36
+ try {
37
+ const tracking = require('promise/setimmediate/rejection-tracking');
38
+ if (tracking && typeof tracking.enable === 'function') {
39
+ tracking.enable({
40
+ allRejections: true,
41
+ onUnhandled: (_id, error) => {
42
+ try {
43
+ emit('js_error', { message: messageOf(error), stack: stackOf(error), unhandled_rejection: true });
44
+ }
45
+ catch {
46
+ }
47
+ },
48
+ onHandled: () => { },
49
+ });
50
+ }
51
+ }
52
+ catch {
53
+ }
54
+ }
55
+ return () => {
56
+ for (const fn of cleanups)
57
+ fn();
58
+ };
59
+ }
@@ -0,0 +1,3 @@
1
+ type Emit = (type: 'fetch_error', payload: Record<string, unknown>) => void;
2
+ export declare function installNetwork(emit: Emit, isOwnEndpoint: (url: string) => boolean, sanitizeUrl: (url: string) => string): () => void;
3
+ export {};
@@ -0,0 +1,47 @@
1
+ export function installNetwork(emit, isOwnEndpoint, sanitizeUrl) {
2
+ const XHR = globalThis.XMLHttpRequest && globalThis.XMLHttpRequest.prototype;
3
+ if (!XHR || typeof XHR.open !== 'function' || typeof XHR.send !== 'function') {
4
+ return () => { };
5
+ }
6
+ const origOpen = XHR.open;
7
+ const origSend = XHR.send;
8
+ XHR.open = function (method, url) {
9
+ try {
10
+ this.__pb = { method, url, start: 0 };
11
+ }
12
+ catch {
13
+ }
14
+ return origOpen.apply(this, arguments);
15
+ };
16
+ XHR.send = function () {
17
+ try {
18
+ const meta = this.__pb;
19
+ if (meta && !isOwnEndpoint(String(meta.url ?? ''))) {
20
+ meta.start = Date.now();
21
+ const xhr = this;
22
+ this.addEventListener('loadend', function () {
23
+ try {
24
+ const status = xhr.status;
25
+ if (status === 0 || status >= 400) {
26
+ emit('fetch_error', {
27
+ method: String(meta.method ?? 'GET').toUpperCase(),
28
+ url: sanitizeUrl(String(meta.url ?? '')),
29
+ status,
30
+ duration_ms: Date.now() - meta.start,
31
+ });
32
+ }
33
+ }
34
+ catch {
35
+ }
36
+ });
37
+ }
38
+ }
39
+ catch {
40
+ }
41
+ return origSend.apply(this, arguments);
42
+ };
43
+ return () => {
44
+ XHR.open = origOpen;
45
+ XHR.send = origSend;
46
+ };
47
+ }
@@ -0,0 +1 @@
1
+ export declare function useProbieExpoRouter(): void;
@@ -0,0 +1,17 @@
1
+ import { useEffect } from 'react';
2
+ import { trackScreen } from '../singleton.js';
3
+ let useSegments = null;
4
+ try {
5
+ useSegments = require('expo-router').useSegments;
6
+ }
7
+ catch {
8
+ useSegments = null;
9
+ }
10
+ export function useProbieExpoRouter() {
11
+ const segments = useSegments ? useSegments() : [];
12
+ const route = Array.isArray(segments) ? segments.join('/') : '';
13
+ useEffect(() => {
14
+ if (useSegments)
15
+ trackScreen(route);
16
+ }, [route]);
17
+ }
@@ -0,0 +1,6 @@
1
+ export interface NavigationContainerBinding {
2
+ ref?: unknown;
3
+ onReady?: () => void;
4
+ onStateChange?: () => void;
5
+ }
6
+ export declare function probieNavigationContainerProps(): NavigationContainerBinding;
@@ -0,0 +1,25 @@
1
+ import { trackScreen } from '../singleton.js';
2
+ let navRef = null;
3
+ try {
4
+ navRef = require('@react-navigation/native').createNavigationContainerRef();
5
+ }
6
+ catch {
7
+ navRef = null;
8
+ }
9
+ export function probieNavigationContainerProps() {
10
+ if (!navRef)
11
+ return {};
12
+ let prev;
13
+ const report = () => {
14
+ try {
15
+ const route = navRef.getCurrentRoute?.();
16
+ if (route && route.name && route.name !== prev) {
17
+ prev = route.name;
18
+ trackScreen(route.name, route.params);
19
+ }
20
+ }
21
+ catch {
22
+ }
23
+ };
24
+ return { ref: navRef, onReady: report, onStateChange: report };
25
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { type PressableProps } from 'react-native';
3
+ export interface ProbiePressableProps extends PressableProps {
4
+ probiePath?: string;
5
+ }
6
+ export declare function ProbiePressable({ probiePath, onPress, ...rest }: ProbiePressableProps): React.ReactElement;
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { Pressable } from 'react-native';
3
+ import { getClient } from '../singleton.js';
4
+ export function ProbiePressable({ probiePath, onPress, ...rest }) {
5
+ const handlePress = (e) => {
6
+ try {
7
+ const source = {
8
+ path: probiePath,
9
+ testID: typeof rest.testID === 'string' ? rest.testID : null,
10
+ accessibilityRole: rest.accessibilityRole ?? null,
11
+ accessibilityLabel: typeof rest.accessibilityLabel === 'string' ? rest.accessibilityLabel : null,
12
+ };
13
+ getClient()?.recordTap(source);
14
+ }
15
+ catch {
16
+ }
17
+ if (typeof onPress === 'function')
18
+ onPress(e);
19
+ };
20
+ return React.createElement(Pressable, { ...rest, onPress: handlePress });
21
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import type { ProbieConfig } from '../types.js';
3
+ export interface ProbieProviderProps {
4
+ config: ProbieConfig;
5
+ children?: React.ReactNode;
6
+ }
7
+ export declare function ProbieProvider({ config, children }: ProbieProviderProps): React.ReactElement;
@@ -0,0 +1,11 @@
1
+ import React, { useEffect } from 'react';
2
+ import { init, __teardown } from '../singleton.js';
3
+ export function ProbieProvider({ config, children }) {
4
+ useEffect(() => {
5
+ init(config);
6
+ return () => {
7
+ __teardown();
8
+ };
9
+ }, [config.token, config.apiBase]);
10
+ return React.createElement(React.Fragment, null, children);
11
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProbieClientImpl } from '../client.js';
2
+ export declare function useProbieClient(): ProbieClientImpl | null;
@@ -0,0 +1,4 @@
1
+ import { getClient } from '../singleton.js';
2
+ export function useProbieClient() {
3
+ return getClient();
4
+ }
@@ -0,0 +1,6 @@
1
+ export interface ProbieFormHandlers {
2
+ onFocus(): void;
3
+ onSubmit(): void;
4
+ formKey: string;
5
+ }
6
+ export declare function useProbieForm(key: string): ProbieFormHandlers;
@@ -0,0 +1,12 @@
1
+ import { useMemo } from 'react';
2
+ import { getClient } from '../singleton.js';
3
+ export function useProbieForm(key) {
4
+ return useMemo(() => {
5
+ const source = { path: key };
6
+ return {
7
+ formKey: key,
8
+ onFocus: () => getClient()?.touchForm(key, source),
9
+ onSubmit: () => getClient()?.submitForm(key, source),
10
+ };
11
+ }, [key]);
12
+ }
@@ -0,0 +1,7 @@
1
+ import type { AsyncStorageLike } from './types.js';
2
+ export interface Session {
3
+ getId(): string;
4
+ readonly ready: Promise<void>;
5
+ rotate(): void;
6
+ }
7
+ export declare function createSession(storage: AsyncStorageLike, idleTimeoutMs: number | null): Session;
@@ -0,0 +1,36 @@
1
+ import { genId } from './ids.js';
2
+ const SID_KEY = 'probie_sid';
3
+ const SID_TS_KEY = 'probie_sid_ts';
4
+ export function createSession(storage, idleTimeoutMs) {
5
+ let id = genId();
6
+ const ready = (async () => {
7
+ try {
8
+ const now = Date.now();
9
+ if (idleTimeoutMs != null) {
10
+ const stored = await storage.getItem(SID_KEY);
11
+ const tsRaw = await storage.getItem(SID_TS_KEY);
12
+ const last = tsRaw ? Number(tsRaw) : 0;
13
+ if (stored && Number.isFinite(last) && last > 0 && now - last < idleTimeoutMs) {
14
+ id = stored;
15
+ }
16
+ }
17
+ await storage.setItem(SID_KEY, id);
18
+ await storage.setItem(SID_TS_KEY, String(now));
19
+ }
20
+ catch {
21
+ }
22
+ })();
23
+ return {
24
+ getId: () => id,
25
+ ready,
26
+ rotate: () => {
27
+ id = genId();
28
+ try {
29
+ void storage.setItem(SID_KEY, id);
30
+ void storage.setItem(SID_TS_KEY, String(Date.now()));
31
+ }
32
+ catch {
33
+ }
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,10 @@
1
+ import { ProbieClientImpl } from './client.js';
2
+ import type { ElementInfo, ElementSource, EventType, ProbieClient, ProbieConfig } from './types.js';
3
+ export declare function init(config: ProbieConfig): ProbieClient;
4
+ export declare function getClient(): ProbieClientImpl | null;
5
+ export declare function identify(userId: string | null): void;
6
+ export declare function reset(): void;
7
+ export declare function trackScreen(name: string, params?: Record<string, unknown>): void;
8
+ export declare function track(type: EventType, payload?: Record<string, unknown>, element?: ElementSource | ElementInfo | null): void;
9
+ export declare function flush(): Promise<void>;
10
+ export declare function __teardown(): void;
@@ -0,0 +1,30 @@
1
+ import { ProbieClientImpl } from './client.js';
2
+ let singleton = null;
3
+ export function init(config) {
4
+ if (singleton)
5
+ return singleton;
6
+ singleton = new ProbieClientImpl(config);
7
+ return singleton;
8
+ }
9
+ export function getClient() {
10
+ return singleton;
11
+ }
12
+ export function identify(userId) {
13
+ singleton?.identify(userId);
14
+ }
15
+ export function reset() {
16
+ singleton?.reset();
17
+ }
18
+ export function trackScreen(name, params) {
19
+ singleton?.trackScreen(name, params);
20
+ }
21
+ export function track(type, payload, element) {
22
+ singleton?.track(type, payload, element);
23
+ }
24
+ export function flush() {
25
+ return singleton ? singleton.flush() : Promise.resolve();
26
+ }
27
+ export function __teardown() {
28
+ singleton?.dispose();
29
+ singleton = null;
30
+ }
@@ -0,0 +1,9 @@
1
+ import type { ProbieEvent } from './types.js';
2
+ export declare const MAX_BATCH = 100;
3
+ export declare const MAX_BODY_BYTES = 262144;
4
+ export declare const MAX_EVENT_BYTES = 8192;
5
+ export declare function utf8Len(s: string): number;
6
+ export declare function buildBatches(events: ProbieEvent[], tokenBytes: number): ProbieEvent[][];
7
+ type FetchImpl = (url: string, init: unknown) => Promise<unknown>;
8
+ export declare function sendEvents(endpoint: string, token: string, events: ProbieEvent[], fetchImpl: FetchImpl): Promise<void>;
9
+ export {};
@@ -0,0 +1,67 @@
1
+ export const MAX_BATCH = 100;
2
+ export const MAX_BODY_BYTES = 262144;
3
+ export const MAX_EVENT_BYTES = 8192;
4
+ const ENVELOPE_OVERHEAD = 40;
5
+ export function utf8Len(s) {
6
+ let n = 0;
7
+ for (let i = 0; i < s.length; i++) {
8
+ const c = s.charCodeAt(i);
9
+ if (c < 0x80)
10
+ n += 1;
11
+ else if (c < 0x800)
12
+ n += 2;
13
+ else if (c >= 0xd800 && c <= 0xdbff) {
14
+ n += 4;
15
+ i++;
16
+ }
17
+ else
18
+ n += 3;
19
+ }
20
+ return n;
21
+ }
22
+ export function buildBatches(events, tokenBytes) {
23
+ const sized = [];
24
+ for (const event of events) {
25
+ const bytes = utf8Len(JSON.stringify(event));
26
+ if (bytes > MAX_EVENT_BYTES)
27
+ continue;
28
+ sized.push({ event, bytes });
29
+ }
30
+ const batches = [];
31
+ let cur = [];
32
+ let curBytes = ENVELOPE_OVERHEAD + tokenBytes;
33
+ for (const { event, bytes } of sized) {
34
+ const withComma = bytes + 1;
35
+ const overflow = curBytes + withComma > MAX_BODY_BYTES;
36
+ if (cur.length > 0 && (cur.length >= MAX_BATCH || overflow)) {
37
+ batches.push(cur);
38
+ cur = [];
39
+ curBytes = ENVELOPE_OVERHEAD + tokenBytes;
40
+ }
41
+ cur.push(event);
42
+ curBytes += withComma;
43
+ }
44
+ if (cur.length > 0)
45
+ batches.push(cur);
46
+ return batches;
47
+ }
48
+ async function postBatch(endpoint, token, batch, fetchImpl) {
49
+ try {
50
+ const body = JSON.stringify({ token, events: batch });
51
+ await fetchImpl(endpoint, {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json', 'X-Widget-Token': token },
54
+ body,
55
+ });
56
+ }
57
+ catch {
58
+ }
59
+ }
60
+ export async function sendEvents(endpoint, token, events, fetchImpl) {
61
+ if (events.length === 0)
62
+ return;
63
+ const batches = buildBatches(events, utf8Len(token));
64
+ for (const batch of batches) {
65
+ await postBatch(endpoint, token, batch, fetchImpl);
66
+ }
67
+ }