@od-oneapp/analytics 2026.1.1301
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 +509 -0
- package/dist/ai-YMnynb-t.mjs +3347 -0
- package/dist/ai-YMnynb-t.mjs.map +1 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client-CTzJVFU5.mjs +9 -0
- package/dist/client-CTzJVFU5.mjs.map +1 -0
- package/dist/client-CcFTauAh.mjs +54 -0
- package/dist/client-CcFTauAh.mjs.map +1 -0
- package/dist/client-CeOLjbac.mjs +281 -0
- package/dist/client-CeOLjbac.mjs.map +1 -0
- package/dist/client-D339NFJS.mjs +267 -0
- package/dist/client-D339NFJS.mjs.map +1 -0
- package/dist/client-next.d.mts +62 -0
- package/dist/client-next.d.mts.map +1 -0
- package/dist/client-next.mjs +525 -0
- package/dist/client-next.mjs.map +1 -0
- package/dist/client.d.mts +30 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +186 -0
- package/dist/client.mjs.map +1 -0
- package/dist/config-DPS6bSYo.d.mts +34 -0
- package/dist/config-DPS6bSYo.d.mts.map +1 -0
- package/dist/config-P6P5adJg.mjs +287 -0
- package/dist/config-P6P5adJg.mjs.map +1 -0
- package/dist/console-8bND3mMU.mjs +128 -0
- package/dist/console-8bND3mMU.mjs.map +1 -0
- package/dist/ecommerce-Cgu4wlux.mjs +993 -0
- package/dist/ecommerce-Cgu4wlux.mjs.map +1 -0
- package/dist/emitters-6-nKo8i-.mjs +208 -0
- package/dist/emitters-6-nKo8i-.mjs.map +1 -0
- package/dist/emitters-DldkVSPp.d.mts +12 -0
- package/dist/emitters-DldkVSPp.d.mts.map +1 -0
- package/dist/index-BfNWgfa5.d.mts +1494 -0
- package/dist/index-BfNWgfa5.d.mts.map +1 -0
- package/dist/index-BkIWe--N.d.mts +953 -0
- package/dist/index-BkIWe--N.d.mts.map +1 -0
- package/dist/index-jPzXRn52.d.mts +184 -0
- package/dist/index-jPzXRn52.d.mts.map +1 -0
- package/dist/manager-DvRRjza6.d.mts +76 -0
- package/dist/manager-DvRRjza6.d.mts.map +1 -0
- package/dist/posthog-bootstrap-CYfIy_WS.mjs +1769 -0
- package/dist/posthog-bootstrap-CYfIy_WS.mjs.map +1 -0
- package/dist/posthog-bootstrap-DWxFrxlt.d.mts +81 -0
- package/dist/posthog-bootstrap-DWxFrxlt.d.mts.map +1 -0
- package/dist/providers-http-client.d.mts +37 -0
- package/dist/providers-http-client.d.mts.map +1 -0
- package/dist/providers-http-client.mjs +320 -0
- package/dist/providers-http-client.mjs.map +1 -0
- package/dist/providers-http-server.d.mts +31 -0
- package/dist/providers-http-server.d.mts.map +1 -0
- package/dist/providers-http-server.mjs +297 -0
- package/dist/providers-http-server.mjs.map +1 -0
- package/dist/providers-http.d.mts +46 -0
- package/dist/providers-http.d.mts.map +1 -0
- package/dist/providers-http.mjs +4 -0
- package/dist/server-edge.d.mts +9 -0
- package/dist/server-edge.d.mts.map +1 -0
- package/dist/server-edge.mjs +373 -0
- package/dist/server-edge.mjs.map +1 -0
- package/dist/server-next.d.mts +67 -0
- package/dist/server-next.d.mts.map +1 -0
- package/dist/server-next.mjs +193 -0
- package/dist/server-next.mjs.map +1 -0
- package/dist/server.d.mts +10 -0
- package/dist/server.mjs +7 -0
- package/dist/service-cYtBBL8x.mjs +945 -0
- package/dist/service-cYtBBL8x.mjs.map +1 -0
- package/dist/shared.d.mts +16 -0
- package/dist/shared.d.mts.map +1 -0
- package/dist/shared.mjs +93 -0
- package/dist/shared.mjs.map +1 -0
- package/dist/types-BxBnNQ0V.d.mts +354 -0
- package/dist/types-BxBnNQ0V.d.mts.map +1 -0
- package/dist/types-CBvxUEaF.d.mts +216 -0
- package/dist/types-CBvxUEaF.d.mts.map +1 -0
- package/dist/types.d.mts +4 -0
- package/dist/types.mjs +0 -0
- package/dist/vercel-types-lwakUfoI.d.mts +102 -0
- package/dist/vercel-types-lwakUfoI.d.mts.map +1 -0
- package/package.json +129 -0
- package/src/client/index.ts +164 -0
- package/src/client/manager.ts +71 -0
- package/src/client/next/components.tsx +270 -0
- package/src/client/next/hooks.ts +217 -0
- package/src/client/next/manager.ts +141 -0
- package/src/client/next.ts +144 -0
- package/src/client-next.ts +101 -0
- package/src/client.ts +89 -0
- package/src/examples/ai-sdk-patterns.ts +583 -0
- package/src/examples/emitter-patterns.ts +476 -0
- package/src/examples/nextjs-emitter-patterns.tsx +403 -0
- package/src/next/app-router.tsx +564 -0
- package/src/next/client.ts +419 -0
- package/src/next/index.ts +84 -0
- package/src/next/middleware.ts +429 -0
- package/src/next/rsc.tsx +300 -0
- package/src/next/server.ts +253 -0
- package/src/next/types.d.ts +220 -0
- package/src/providers/base-provider.ts +419 -0
- package/src/providers/console/client.ts +10 -0
- package/src/providers/console/index.ts +152 -0
- package/src/providers/console/server.ts +6 -0
- package/src/providers/console/types.ts +15 -0
- package/src/providers/http/client.ts +464 -0
- package/src/providers/http/index.ts +30 -0
- package/src/providers/http/server.ts +396 -0
- package/src/providers/http/types.ts +135 -0
- package/src/providers/posthog/client.ts +518 -0
- package/src/providers/posthog/index.ts +11 -0
- package/src/providers/posthog/server.ts +329 -0
- package/src/providers/posthog/types.ts +104 -0
- package/src/providers/segment/client.ts +113 -0
- package/src/providers/segment/index.ts +11 -0
- package/src/providers/segment/server.ts +115 -0
- package/src/providers/segment/types.ts +51 -0
- package/src/providers/vercel/client.ts +102 -0
- package/src/providers/vercel/index.ts +11 -0
- package/src/providers/vercel/server.ts +89 -0
- package/src/providers/vercel/types.ts +27 -0
- package/src/server/index.ts +103 -0
- package/src/server/manager.ts +62 -0
- package/src/server/next.ts +210 -0
- package/src/server-edge.ts +442 -0
- package/src/server-next.ts +39 -0
- package/src/server.ts +106 -0
- package/src/shared/emitters/ai/README.md +981 -0
- package/src/shared/emitters/ai/events/agent.ts +130 -0
- package/src/shared/emitters/ai/events/artifacts.ts +167 -0
- package/src/shared/emitters/ai/events/chat.ts +126 -0
- package/src/shared/emitters/ai/events/chatbot-ecommerce.ts +133 -0
- package/src/shared/emitters/ai/events/completion.ts +103 -0
- package/src/shared/emitters/ai/events/content-generation.ts +347 -0
- package/src/shared/emitters/ai/events/conversation.ts +332 -0
- package/src/shared/emitters/ai/events/product-features.ts +1402 -0
- package/src/shared/emitters/ai/events/streaming.ts +114 -0
- package/src/shared/emitters/ai/events/tool.ts +93 -0
- package/src/shared/emitters/ai/index.ts +69 -0
- package/src/shared/emitters/ai/track-ai-sdk.ts +74 -0
- package/src/shared/emitters/ai/track-ai.ts +50 -0
- package/src/shared/emitters/ai/types.ts +1041 -0
- package/src/shared/emitters/ai/utils.ts +468 -0
- package/src/shared/emitters/ecommerce/events/cart-checkout.ts +106 -0
- package/src/shared/emitters/ecommerce/events/coupon.ts +49 -0
- package/src/shared/emitters/ecommerce/events/engagement.ts +61 -0
- package/src/shared/emitters/ecommerce/events/marketplace.ts +119 -0
- package/src/shared/emitters/ecommerce/events/order.ts +199 -0
- package/src/shared/emitters/ecommerce/events/product.ts +205 -0
- package/src/shared/emitters/ecommerce/events/registry.ts +123 -0
- package/src/shared/emitters/ecommerce/events/wishlist-sharing.ts +140 -0
- package/src/shared/emitters/ecommerce/index.ts +46 -0
- package/src/shared/emitters/ecommerce/track-ecommerce.ts +53 -0
- package/src/shared/emitters/ecommerce/types.ts +314 -0
- package/src/shared/emitters/ecommerce/utils.ts +216 -0
- package/src/shared/emitters/emitter-types.ts +974 -0
- package/src/shared/emitters/emitters.ts +292 -0
- package/src/shared/emitters/helpers.ts +419 -0
- package/src/shared/emitters/index.ts +66 -0
- package/src/shared/index.ts +142 -0
- package/src/shared/ingestion/index.ts +66 -0
- package/src/shared/ingestion/schemas.ts +386 -0
- package/src/shared/ingestion/service.ts +628 -0
- package/src/shared/node22-features.ts +848 -0
- package/src/shared/providers/console-provider.ts +160 -0
- package/src/shared/types/base-types.ts +54 -0
- package/src/shared/types/console-types.ts +19 -0
- package/src/shared/types/posthog-types.ts +131 -0
- package/src/shared/types/segment-types.ts +15 -0
- package/src/shared/types/types.ts +397 -0
- package/src/shared/types/vercel-types.ts +19 -0
- package/src/shared/utils/config-client.ts +19 -0
- package/src/shared/utils/config.ts +250 -0
- package/src/shared/utils/emitter-adapter.ts +212 -0
- package/src/shared/utils/manager.test.ts +36 -0
- package/src/shared/utils/manager.ts +1322 -0
- package/src/shared/utils/posthog-bootstrap.ts +136 -0
- package/src/shared/utils/posthog-client-utils.ts +48 -0
- package/src/shared/utils/posthog-next-utils.ts +282 -0
- package/src/shared/utils/posthog-server-utils.ts +210 -0
- package/src/shared/utils/rate-limit.ts +289 -0
- package/src/shared/utils/security.ts +545 -0
- package/src/shared/utils/validation-client.ts +161 -0
- package/src/shared/utils/validation.ts +399 -0
- package/src/shared.ts +155 -0
- package/src/types/index.ts +62 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Console provider - works in both client and server environments
|
|
3
|
+
*
|
|
4
|
+
* Provides console-based analytics logging for development and debugging.
|
|
5
|
+
* Useful for testing analytics events without sending data to external services.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* This provider:
|
|
9
|
+
* - Logs analytics events to console
|
|
10
|
+
* - Works in both browser and Node.js environments
|
|
11
|
+
* - Useful for development and debugging
|
|
12
|
+
* - Integrates with observability package for structured logging
|
|
13
|
+
*
|
|
14
|
+
* @module @od-oneapp/analytics/providers/console
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { logDebug, logWarn } from '@repo/shared/logger';
|
|
18
|
+
|
|
19
|
+
import type { ConsoleConfig } from './types';
|
|
20
|
+
import type { AnalyticsProvider, ProviderConfig } from '../../shared/types/types';
|
|
21
|
+
|
|
22
|
+
export class ConsoleProvider implements AnalyticsProvider {
|
|
23
|
+
readonly name = 'console';
|
|
24
|
+
private config: ConsoleConfig;
|
|
25
|
+
private isInitialized = false;
|
|
26
|
+
|
|
27
|
+
constructor(config: ProviderConfig) {
|
|
28
|
+
const options = config.options as ConsoleConfig | undefined;
|
|
29
|
+
this.config = {
|
|
30
|
+
prefix: '[Analytics]',
|
|
31
|
+
logLevel: 'info',
|
|
32
|
+
colorize: false,
|
|
33
|
+
...options,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async initialize(): Promise<void> {
|
|
38
|
+
if (this.isInitialized) return;
|
|
39
|
+
|
|
40
|
+
// Reduce noisy logs: only log in development or explicit debug
|
|
41
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
42
|
+
const isDebug = process.env.NEXT_PUBLIC_OBSERVABILITY_DEBUG === 'true';
|
|
43
|
+
if (isDev || isDebug) {
|
|
44
|
+
this.log('Console Analytics Provider initialized', {
|
|
45
|
+
config: this.config,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.isInitialized = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async track(event: string, properties: any = {}, _context?: any): Promise<void> {
|
|
53
|
+
if (!this.isInitialized) {
|
|
54
|
+
logWarn('Console provider not initialized', {
|
|
55
|
+
provider: 'console',
|
|
56
|
+
operation: 'track',
|
|
57
|
+
event,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.log('TRACK', {
|
|
63
|
+
event,
|
|
64
|
+
properties,
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async identify(userId: string, traits: any = {}, _context?: any): Promise<void> {
|
|
70
|
+
if (!this.isInitialized) {
|
|
71
|
+
logWarn('Console provider not initialized', {
|
|
72
|
+
provider: 'console',
|
|
73
|
+
operation: 'identify',
|
|
74
|
+
userId,
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.log('IDENTIFY', {
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
traits,
|
|
82
|
+
userId,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async page(name?: string, properties: any = {}, _context?: any): Promise<void> {
|
|
87
|
+
if (!this.isInitialized) {
|
|
88
|
+
logWarn('Console provider not initialized', {
|
|
89
|
+
provider: 'console',
|
|
90
|
+
operation: 'page',
|
|
91
|
+
metadata: { name },
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.log('PAGE', {
|
|
97
|
+
name,
|
|
98
|
+
properties,
|
|
99
|
+
timestamp: new Date().toISOString(),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async group(groupId: string, traits: any = {}, _context?: any): Promise<void> {
|
|
104
|
+
if (!this.isInitialized) {
|
|
105
|
+
logWarn('Console provider not initialized', {
|
|
106
|
+
provider: 'console',
|
|
107
|
+
operation: 'group',
|
|
108
|
+
metadata: { groupId },
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.log('GROUP', {
|
|
114
|
+
groupId,
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
traits,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async alias(userId: string, previousId: string, _context?: any): Promise<void> {
|
|
121
|
+
if (!this.isInitialized) {
|
|
122
|
+
logWarn('Console provider not initialized', {
|
|
123
|
+
provider: 'console',
|
|
124
|
+
operation: 'alias',
|
|
125
|
+
userId,
|
|
126
|
+
metadata: { previousId },
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.log('ALIAS', {
|
|
132
|
+
previousId,
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
userId,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private log(action: string, data: any): void {
|
|
139
|
+
const { logLevel, prefix } = this.config;
|
|
140
|
+
|
|
141
|
+
if (logLevel === 'error') return; // Don't log analytics in error-only mode
|
|
142
|
+
|
|
143
|
+
const message = `${prefix} ${action}`;
|
|
144
|
+
|
|
145
|
+
// Use analytics logger instead of console directly
|
|
146
|
+
logDebug(message, {
|
|
147
|
+
provider: 'console',
|
|
148
|
+
operation: action.toLowerCase(),
|
|
149
|
+
metadata: data,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Console provider type definitions
|
|
3
|
+
*
|
|
4
|
+
* Defines TypeScript types and interfaces for Console analytics provider configuration.
|
|
5
|
+
*
|
|
6
|
+
* @module @od-oneapp/analytics/providers/console/types
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ConsoleConfig {
|
|
10
|
+
// Console provider doesn't need configuration
|
|
11
|
+
// All options are optional
|
|
12
|
+
prefix?: string;
|
|
13
|
+
logLevel?: 'debug' | 'info' | 'warn' | 'error';
|
|
14
|
+
colorize?: boolean;
|
|
15
|
+
}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HTTP provider for browser/client environments
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP-based analytics event sending for client-side applications.
|
|
5
|
+
* Sends events to a remote ingestion endpoint (e.g., oneapp-api/ingest).
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Event batching for efficiency
|
|
9
|
+
* - Automatic flush on interval, batch size, or page unload
|
|
10
|
+
* - Retry with exponential backoff
|
|
11
|
+
* - Anonymous ID persistence in localStorage
|
|
12
|
+
* - Offline queue support (events queued when offline)
|
|
13
|
+
*
|
|
14
|
+
* @module @od-oneapp/analytics/providers/http/client
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { logInfo, logWarn, logError } from '@repo/shared/logs';
|
|
18
|
+
|
|
19
|
+
import type { HttpProviderConfig, IngestionResponse, QueuedEvent } from './types';
|
|
20
|
+
import type {
|
|
21
|
+
AnalyticsContext,
|
|
22
|
+
AnalyticsProvider,
|
|
23
|
+
GroupTraits,
|
|
24
|
+
PageProperties,
|
|
25
|
+
Properties,
|
|
26
|
+
ProviderConfig,
|
|
27
|
+
UserTraits,
|
|
28
|
+
} from '../../shared/types/types';
|
|
29
|
+
|
|
30
|
+
/** Default configuration values */
|
|
31
|
+
const DEFAULTS = {
|
|
32
|
+
batchSize: 10,
|
|
33
|
+
flushInterval: 5000,
|
|
34
|
+
timeout: 10000,
|
|
35
|
+
retries: 3,
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
/** Base delay for exponential backoff in ms */
|
|
39
|
+
const BACKOFF_BASE_MS = 1000;
|
|
40
|
+
|
|
41
|
+
/** LocalStorage key for anonymous ID */
|
|
42
|
+
const ANON_ID_KEY = 'analytics_anonymous_id';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* HTTP Analytics Provider for browser environments.
|
|
46
|
+
*
|
|
47
|
+
* Sends analytics events to a remote endpoint via HTTP POST requests.
|
|
48
|
+
* Events are batched and flushed periodically for efficiency.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const provider = new HttpClientProvider({
|
|
53
|
+
* options: {
|
|
54
|
+
* endpoint: 'https://api.oneapp.dev/v1/ingest',
|
|
55
|
+
* apiKey: process.env.NEXT_PUBLIC_ANALYTICS_API_KEY,
|
|
56
|
+
* },
|
|
57
|
+
* });
|
|
58
|
+
* await provider.initialize();
|
|
59
|
+
* await provider.track('Button Clicked', { button: 'signup' });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export class HttpClientProvider implements AnalyticsProvider {
|
|
63
|
+
readonly name = 'http';
|
|
64
|
+
|
|
65
|
+
private config: Required<
|
|
66
|
+
Pick<HttpProviderConfig, 'batchSize' | 'flushInterval' | 'timeout' | 'retries'>
|
|
67
|
+
> &
|
|
68
|
+
HttpProviderConfig;
|
|
69
|
+
private isInitialized = false;
|
|
70
|
+
private queue: QueuedEvent[] = [];
|
|
71
|
+
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
72
|
+
private userId?: string;
|
|
73
|
+
private anonymousId: string;
|
|
74
|
+
private isFlushing = false;
|
|
75
|
+
|
|
76
|
+
constructor(providerConfig: ProviderConfig) {
|
|
77
|
+
const options = (providerConfig.options ?? {}) as unknown as HttpProviderConfig;
|
|
78
|
+
|
|
79
|
+
if (!options.endpoint) {
|
|
80
|
+
throw new Error('HttpProvider requires an endpoint URL');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.config = {
|
|
84
|
+
...options,
|
|
85
|
+
batchSize: options.batchSize ?? DEFAULTS.batchSize,
|
|
86
|
+
flushInterval: options.flushInterval ?? DEFAULTS.flushInterval,
|
|
87
|
+
timeout: options.timeout ?? DEFAULTS.timeout,
|
|
88
|
+
retries: options.retries ?? DEFAULTS.retries,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.userId = options.userId;
|
|
92
|
+
this.anonymousId = options.anonymousId ?? this.getOrCreateAnonymousId();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async initialize(): Promise<void> {
|
|
96
|
+
if (this.isInitialized) return;
|
|
97
|
+
|
|
98
|
+
// Start auto-flush timer if interval > 0
|
|
99
|
+
if (this.config.flushInterval > 0) {
|
|
100
|
+
this.flushTimer = setInterval(() => {
|
|
101
|
+
void this.flush();
|
|
102
|
+
}, this.config.flushInterval);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Flush on page unload/visibility change
|
|
106
|
+
if (typeof document !== 'undefined') {
|
|
107
|
+
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (typeof window !== 'undefined') {
|
|
111
|
+
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (this.config.debug) {
|
|
115
|
+
this.log('HTTP Analytics Provider initialized', {
|
|
116
|
+
endpoint: this.config.endpoint,
|
|
117
|
+
batchSize: this.config.batchSize,
|
|
118
|
+
flushInterval: this.config.flushInterval,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.isInitialized = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async track(
|
|
126
|
+
event: string,
|
|
127
|
+
properties: Properties = {},
|
|
128
|
+
_context?: AnalyticsContext,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
if (!this.isInitialized) {
|
|
131
|
+
this.warn('HTTP provider not initialized', { operation: 'track', event });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.enqueue({
|
|
136
|
+
type: 'track',
|
|
137
|
+
event,
|
|
138
|
+
properties,
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
userId: this.userId,
|
|
141
|
+
anonymousId: this.anonymousId,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async identify(
|
|
146
|
+
userId: string,
|
|
147
|
+
traits: UserTraits = {},
|
|
148
|
+
_context?: AnalyticsContext,
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
if (!this.isInitialized) {
|
|
151
|
+
this.warn('HTTP provider not initialized', { operation: 'identify', userId });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Update stored user ID
|
|
156
|
+
this.userId = userId;
|
|
157
|
+
|
|
158
|
+
this.enqueue({
|
|
159
|
+
type: 'identify',
|
|
160
|
+
userId,
|
|
161
|
+
traits,
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
anonymousId: this.anonymousId,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async page(
|
|
168
|
+
name?: string,
|
|
169
|
+
properties: PageProperties = {},
|
|
170
|
+
_context?: AnalyticsContext,
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
if (!this.isInitialized) {
|
|
173
|
+
this.warn('HTTP provider not initialized', { operation: 'page', name });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.enqueue({
|
|
178
|
+
type: 'page',
|
|
179
|
+
name,
|
|
180
|
+
properties,
|
|
181
|
+
timestamp: new Date().toISOString(),
|
|
182
|
+
userId: this.userId,
|
|
183
|
+
anonymousId: this.anonymousId,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async group(
|
|
188
|
+
groupId: string,
|
|
189
|
+
traits: GroupTraits = {},
|
|
190
|
+
_context?: AnalyticsContext,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
if (!this.isInitialized) {
|
|
193
|
+
this.warn('HTTP provider not initialized', { operation: 'group', groupId });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.enqueue({
|
|
198
|
+
type: 'group',
|
|
199
|
+
groupId,
|
|
200
|
+
traits,
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
userId: this.userId,
|
|
203
|
+
anonymousId: this.anonymousId,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async alias(userId: string, previousId: string, _context?: AnalyticsContext): Promise<void> {
|
|
208
|
+
if (!this.isInitialized) {
|
|
209
|
+
this.warn('HTTP provider not initialized', { operation: 'alias', userId, previousId });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.enqueue({
|
|
214
|
+
type: 'alias',
|
|
215
|
+
userId,
|
|
216
|
+
previousId,
|
|
217
|
+
timestamp: new Date().toISOString(),
|
|
218
|
+
anonymousId: this.anonymousId,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Flush all queued events to the remote endpoint.
|
|
224
|
+
* Called automatically on interval, batch size, or page events.
|
|
225
|
+
*/
|
|
226
|
+
async flush(): Promise<void> {
|
|
227
|
+
if (this.queue.length === 0 || this.isFlushing) return;
|
|
228
|
+
|
|
229
|
+
this.isFlushing = true;
|
|
230
|
+
|
|
231
|
+
// Take all events from queue
|
|
232
|
+
const events = [...this.queue];
|
|
233
|
+
this.queue = [];
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await this.sendBatch(events);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
// Re-queue events on failure (they'll be retried on next flush)
|
|
239
|
+
this.queue.unshift(...events);
|
|
240
|
+
this.error('Failed to flush events, re-queued for retry', {
|
|
241
|
+
eventCount: events.length,
|
|
242
|
+
error: error instanceof Error ? error.message : String(error),
|
|
243
|
+
});
|
|
244
|
+
} finally {
|
|
245
|
+
this.isFlushing = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Shutdown the provider gracefully.
|
|
251
|
+
* Flushes remaining events and removes event listeners.
|
|
252
|
+
*/
|
|
253
|
+
async shutdown(): Promise<void> {
|
|
254
|
+
if (this.flushTimer) {
|
|
255
|
+
clearInterval(this.flushTimer);
|
|
256
|
+
this.flushTimer = null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (typeof document !== 'undefined') {
|
|
260
|
+
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (typeof window !== 'undefined') {
|
|
264
|
+
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Final flush
|
|
268
|
+
await this.flush();
|
|
269
|
+
this.isInitialized = false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get the current queue length (for testing/monitoring).
|
|
274
|
+
*/
|
|
275
|
+
getQueueLength(): number {
|
|
276
|
+
return this.queue.length;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ============================================================================
|
|
280
|
+
// Event Handlers
|
|
281
|
+
// ============================================================================
|
|
282
|
+
|
|
283
|
+
private handleVisibilityChange = (): void => {
|
|
284
|
+
if (document.visibilityState === 'hidden') {
|
|
285
|
+
void this.flush();
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
private handleBeforeUnload = (): void => {
|
|
290
|
+
// Use sendBeacon for reliable delivery on page unload
|
|
291
|
+
if (this.queue.length > 0 && typeof navigator?.sendBeacon === 'function') {
|
|
292
|
+
const payload = JSON.stringify({ batch: this.queue });
|
|
293
|
+
const headers: Record<string, string> = {
|
|
294
|
+
type: 'application/json',
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Note: sendBeacon doesn't support custom headers, so we include API key in payload
|
|
298
|
+
// The server should accept it from either header or body
|
|
299
|
+
const blob = new Blob([payload], headers);
|
|
300
|
+
navigator.sendBeacon(this.config.endpoint, blob);
|
|
301
|
+
this.queue = [];
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// ============================================================================
|
|
306
|
+
// Private Methods
|
|
307
|
+
// ============================================================================
|
|
308
|
+
|
|
309
|
+
private enqueue(event: QueuedEvent): void {
|
|
310
|
+
this.queue.push(event);
|
|
311
|
+
|
|
312
|
+
if (this.config.debug) {
|
|
313
|
+
this.log('Event queued', {
|
|
314
|
+
type: event.type,
|
|
315
|
+
queueLength: this.queue.length,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Flush if batch size reached
|
|
320
|
+
if (this.queue.length >= this.config.batchSize) {
|
|
321
|
+
void this.flush();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async sendBatch(events: QueuedEvent[]): Promise<void> {
|
|
326
|
+
const payload = { batch: events };
|
|
327
|
+
|
|
328
|
+
let lastError: Error | null = null;
|
|
329
|
+
|
|
330
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
331
|
+
try {
|
|
332
|
+
const response = await this.sendRequest(payload);
|
|
333
|
+
|
|
334
|
+
if (response.success) {
|
|
335
|
+
if (this.config.debug) {
|
|
336
|
+
this.log('Batch sent successfully', {
|
|
337
|
+
accepted: response.accepted,
|
|
338
|
+
rejected: response.rejected,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Request succeeded but ingestion failed
|
|
345
|
+
this.warn('Batch partially rejected', {
|
|
346
|
+
accepted: response.accepted,
|
|
347
|
+
rejected: response.rejected,
|
|
348
|
+
error: response.error,
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
} catch (error) {
|
|
352
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
353
|
+
|
|
354
|
+
if (attempt < this.config.retries) {
|
|
355
|
+
// Exponential backoff: 1s, 2s, 4s, ...
|
|
356
|
+
const delay = BACKOFF_BASE_MS * Math.pow(2, attempt);
|
|
357
|
+
|
|
358
|
+
if (this.config.debug) {
|
|
359
|
+
this.log('Retrying batch send', {
|
|
360
|
+
attempt: attempt + 1,
|
|
361
|
+
maxRetries: this.config.retries,
|
|
362
|
+
delayMs: delay,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
await this.sleep(delay);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// All retries exhausted
|
|
372
|
+
throw lastError ?? new Error('Failed to send batch after retries');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private async sendRequest(payload: { batch: QueuedEvent[] }): Promise<IngestionResponse> {
|
|
376
|
+
const headers: Record<string, string> = {
|
|
377
|
+
'Content-Type': 'application/json',
|
|
378
|
+
...this.config.headers,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
if (this.config.apiKey) {
|
|
382
|
+
headers['X-API-Key'] = this.config.apiKey;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const controller = new AbortController();
|
|
386
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const response = await fetch(this.config.endpoint, {
|
|
390
|
+
method: 'POST',
|
|
391
|
+
headers,
|
|
392
|
+
body: JSON.stringify(payload),
|
|
393
|
+
signal: controller.signal,
|
|
394
|
+
// Include credentials for cross-origin requests if needed
|
|
395
|
+
credentials: 'omit',
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (!response.ok) {
|
|
399
|
+
// Handle rate limiting
|
|
400
|
+
if (response.status === 429) {
|
|
401
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
402
|
+
throw new Error(`Rate limited. Retry after ${retryAfter ?? 'unknown'} seconds`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const errorBody = await response.text();
|
|
406
|
+
throw new Error(`HTTP ${response.status}: ${errorBody}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return (await response.json()) as IngestionResponse;
|
|
410
|
+
} finally {
|
|
411
|
+
clearTimeout(timeoutId);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private getOrCreateAnonymousId(): string {
|
|
416
|
+
// Try to get from localStorage
|
|
417
|
+
if (typeof localStorage !== 'undefined') {
|
|
418
|
+
try {
|
|
419
|
+
const stored = localStorage.getItem(ANON_ID_KEY);
|
|
420
|
+
if (stored) return stored;
|
|
421
|
+
|
|
422
|
+
const newId = this.generateAnonymousId();
|
|
423
|
+
localStorage.setItem(ANON_ID_KEY, newId);
|
|
424
|
+
return newId;
|
|
425
|
+
} catch {
|
|
426
|
+
// localStorage not available or blocked
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return this.generateAnonymousId();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private generateAnonymousId(): string {
|
|
434
|
+
// Use crypto.randomUUID if available
|
|
435
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
436
|
+
return crypto.randomUUID();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Fallback
|
|
440
|
+
return `anon_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 9)}`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private sleep(ms: number): Promise<void> {
|
|
444
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ============================================================================
|
|
448
|
+
// Logging (browser-compatible)
|
|
449
|
+
// ============================================================================
|
|
450
|
+
|
|
451
|
+
private log(message: string, data?: Record<string, unknown>): void {
|
|
452
|
+
if (this.config.debug) {
|
|
453
|
+
logInfo(`[Analytics:HTTP] ${message}`, data);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private warn(message: string, data?: Record<string, unknown>): void {
|
|
458
|
+
logWarn(`[Analytics:HTTP] ${message}`, data);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private error(message: string, data?: Record<string, unknown>): void {
|
|
462
|
+
logError(`[Analytics:HTTP] ${message}`, data);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HTTP Analytics Provider
|
|
3
|
+
*
|
|
4
|
+
* Exports HTTP-based analytics providers for sending events to a remote
|
|
5
|
+
* ingestion endpoint (e.g., oneapp-api's /api/v1/ingest).
|
|
6
|
+
*
|
|
7
|
+
* Use `HttpClientProvider` for browser environments and `HttpServerProvider`
|
|
8
|
+
* for Node.js/server environments.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // Client-side (browser)
|
|
13
|
+
* import { HttpClientProvider } from '@od-oneapp/analytics/providers/http/client';
|
|
14
|
+
*
|
|
15
|
+
* // Server-side (Node.js)
|
|
16
|
+
* import { HttpServerProvider } from '@od-oneapp/analytics/providers/http/server';
|
|
17
|
+
*
|
|
18
|
+
* // Or import types
|
|
19
|
+
* import type { HttpProviderConfig } from '@od-oneapp/analytics/providers/http';
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @module @od-oneapp/analytics/providers/http
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type { HttpProviderConfig, IngestionResponse, QueuedEvent } from './types';
|
|
26
|
+
export { HttpClientProvider } from './client';
|
|
27
|
+
export { HttpServerProvider } from './server';
|
|
28
|
+
|
|
29
|
+
// Edge-compatible alias - use HttpServerProvider which works in edge environments
|
|
30
|
+
export { HttpServerProvider as HttpProvider } from './server';
|