@grainql/analytics-web 1.0.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.
@@ -0,0 +1,292 @@
1
+ /* Grain Analytics Web SDK v1.0.0 | MIT License | Development Build */
2
+ "use strict";
3
+ var Grain = (() => {
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/index.ts
23
+ var src_exports = {};
24
+ __export(src_exports, {
25
+ GrainAnalytics: () => GrainAnalytics,
26
+ createGrainAnalytics: () => createGrainAnalytics,
27
+ default: () => src_default
28
+ });
29
+ var GrainAnalytics = class {
30
+ constructor(config) {
31
+ this.eventQueue = [];
32
+ this.flushTimer = null;
33
+ this.isDestroyed = false;
34
+ this.config = {
35
+ apiUrl: "https://api.grainql.com",
36
+ authStrategy: "NONE",
37
+ batchSize: 50,
38
+ flushInterval: 5e3,
39
+ // 5 seconds
40
+ retryAttempts: 3,
41
+ retryDelay: 1e3,
42
+ // 1 second
43
+ debug: false,
44
+ ...config,
45
+ tenantId: config.tenantId
46
+ };
47
+ this.validateConfig();
48
+ this.setupBeforeUnload();
49
+ this.startFlushTimer();
50
+ }
51
+ validateConfig() {
52
+ if (!this.config.tenantId) {
53
+ throw new Error("Grain Analytics: tenantId is required");
54
+ }
55
+ if (this.config.authStrategy === "SERVER_SIDE" && !this.config.secretKey) {
56
+ throw new Error("Grain Analytics: secretKey is required for SERVER_SIDE auth strategy");
57
+ }
58
+ if (this.config.authStrategy === "JWT" && !this.config.authProvider) {
59
+ throw new Error("Grain Analytics: authProvider is required for JWT auth strategy");
60
+ }
61
+ }
62
+ log(...args) {
63
+ if (this.config.debug) {
64
+ console.log("[Grain Analytics]", ...args);
65
+ }
66
+ }
67
+ generateInsertId() {
68
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
69
+ }
70
+ formatEvent(event) {
71
+ const timestamp = event.timestamp || /* @__PURE__ */ new Date();
72
+ const eventTs = timestamp.toISOString();
73
+ const eventDate = timestamp.toISOString().split("T")[0];
74
+ return {
75
+ eventName: event.eventName,
76
+ eventTs,
77
+ userId: event.userId || "anonymous",
78
+ properties: event.properties || {},
79
+ eventDate,
80
+ insertId: this.generateInsertId()
81
+ };
82
+ }
83
+ async getAuthHeaders() {
84
+ const headers = {
85
+ "Content-Type": "application/json"
86
+ };
87
+ switch (this.config.authStrategy) {
88
+ case "NONE":
89
+ break;
90
+ case "SERVER_SIDE":
91
+ headers["Authorization"] = `Chase ${this.config.secretKey}`;
92
+ break;
93
+ case "JWT":
94
+ if (this.config.authProvider) {
95
+ const token = await this.config.authProvider.getToken();
96
+ headers["Authorization"] = `Bearer ${token}`;
97
+ }
98
+ break;
99
+ }
100
+ return headers;
101
+ }
102
+ async delay(ms) {
103
+ return new Promise((resolve) => setTimeout(resolve, ms));
104
+ }
105
+ isRetriableError(error) {
106
+ if (error instanceof Error) {
107
+ if (error.message.includes("fetch"))
108
+ return true;
109
+ if (error.message.includes("network"))
110
+ return true;
111
+ }
112
+ if (typeof error === "object" && error !== null && "status" in error) {
113
+ const status = error.status;
114
+ return status >= 500 || status === 429;
115
+ }
116
+ return false;
117
+ }
118
+ async sendEvents(events) {
119
+ if (events.length === 0)
120
+ return;
121
+ let lastError;
122
+ for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
123
+ try {
124
+ const headers = await this.getAuthHeaders();
125
+ const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}`;
126
+ this.log(`Sending ${events.length} events to ${url} (attempt ${attempt + 1})`);
127
+ const response = await fetch(url, {
128
+ method: "POST",
129
+ headers,
130
+ body: JSON.stringify({ events })
131
+ });
132
+ if (!response.ok) {
133
+ let errorMessage = `HTTP ${response.status}`;
134
+ try {
135
+ const errorBody = await response.json();
136
+ if (errorBody?.message) {
137
+ errorMessage = errorBody.message;
138
+ }
139
+ } catch {
140
+ const errorText = await response.text();
141
+ if (errorText) {
142
+ errorMessage = errorText;
143
+ }
144
+ }
145
+ const error = new Error(`Failed to send events: ${errorMessage}`);
146
+ error.status = response.status;
147
+ throw error;
148
+ }
149
+ this.log(`Successfully sent ${events.length} events`);
150
+ return;
151
+ } catch (error) {
152
+ lastError = error;
153
+ if (attempt === this.config.retryAttempts) {
154
+ break;
155
+ }
156
+ if (!this.isRetriableError(error)) {
157
+ break;
158
+ }
159
+ const delayMs = this.config.retryDelay * Math.pow(2, attempt);
160
+ this.log(`Retrying in ${delayMs}ms after error:`, error);
161
+ await this.delay(delayMs);
162
+ }
163
+ }
164
+ console.error("[Grain Analytics] Failed to send events after all retries:", lastError);
165
+ throw lastError;
166
+ }
167
+ async sendEventsWithBeacon(events) {
168
+ if (events.length === 0)
169
+ return;
170
+ try {
171
+ const headers = await this.getAuthHeaders();
172
+ const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}`;
173
+ const body = JSON.stringify({ events });
174
+ if (typeof navigator !== "undefined" && "sendBeacon" in navigator) {
175
+ const blob = new Blob([body], { type: "application/json" });
176
+ const success = navigator.sendBeacon(url, blob);
177
+ if (success) {
178
+ this.log(`Successfully sent ${events.length} events via beacon`);
179
+ return;
180
+ }
181
+ }
182
+ await fetch(url, {
183
+ method: "POST",
184
+ headers,
185
+ body,
186
+ keepalive: true
187
+ });
188
+ this.log(`Successfully sent ${events.length} events via fetch (keepalive)`);
189
+ } catch (error) {
190
+ console.error("[Grain Analytics] Failed to send events via beacon:", error);
191
+ }
192
+ }
193
+ startFlushTimer() {
194
+ if (this.flushTimer) {
195
+ clearInterval(this.flushTimer);
196
+ }
197
+ this.flushTimer = window.setInterval(() => {
198
+ if (this.eventQueue.length > 0) {
199
+ this.flush().catch((error) => {
200
+ console.error("[Grain Analytics] Auto-flush failed:", error);
201
+ });
202
+ }
203
+ }, this.config.flushInterval);
204
+ }
205
+ setupBeforeUnload() {
206
+ if (typeof window === "undefined")
207
+ return;
208
+ const handleBeforeUnload = () => {
209
+ if (this.eventQueue.length > 0) {
210
+ this.sendEventsWithBeacon([...this.eventQueue]).catch(() => {
211
+ });
212
+ this.eventQueue = [];
213
+ }
214
+ };
215
+ window.addEventListener("beforeunload", handleBeforeUnload);
216
+ window.addEventListener("pagehide", handleBeforeUnload);
217
+ document.addEventListener("visibilitychange", () => {
218
+ if (document.visibilityState === "hidden" && this.eventQueue.length > 0) {
219
+ this.sendEventsWithBeacon([...this.eventQueue]).catch(() => {
220
+ });
221
+ this.eventQueue = [];
222
+ }
223
+ });
224
+ }
225
+ async track(eventOrName, propertiesOrOptions, options) {
226
+ if (this.isDestroyed) {
227
+ throw new Error("Grain Analytics: Client has been destroyed");
228
+ }
229
+ let event;
230
+ let opts = {};
231
+ if (typeof eventOrName === "string") {
232
+ event = {
233
+ eventName: eventOrName,
234
+ properties: propertiesOrOptions
235
+ };
236
+ opts = options || {};
237
+ } else {
238
+ event = eventOrName;
239
+ opts = propertiesOrOptions || {};
240
+ }
241
+ const formattedEvent = this.formatEvent(event);
242
+ this.eventQueue.push(formattedEvent);
243
+ this.log(`Queued event: ${event.eventName}`, event.properties);
244
+ if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
245
+ await this.flush();
246
+ }
247
+ }
248
+ /**
249
+ * Identify a user (sets userId for subsequent events)
250
+ */
251
+ identify(userId) {
252
+ this.log(`Identified user: ${userId}`);
253
+ }
254
+ /**
255
+ * Manually flush all queued events
256
+ */
257
+ async flush() {
258
+ if (this.eventQueue.length === 0)
259
+ return;
260
+ const eventsToSend = [...this.eventQueue];
261
+ this.eventQueue = [];
262
+ await this.sendEvents(eventsToSend);
263
+ }
264
+ /**
265
+ * Destroy the client and clean up resources
266
+ */
267
+ destroy() {
268
+ this.isDestroyed = true;
269
+ if (this.flushTimer) {
270
+ clearInterval(this.flushTimer);
271
+ this.flushTimer = null;
272
+ }
273
+ if (this.eventQueue.length > 0) {
274
+ this.sendEventsWithBeacon([...this.eventQueue]).catch(() => {
275
+ });
276
+ this.eventQueue = [];
277
+ }
278
+ }
279
+ };
280
+ function createGrainAnalytics(config) {
281
+ return new GrainAnalytics(config);
282
+ }
283
+ var src_default = GrainAnalytics;
284
+ if (typeof window !== "undefined") {
285
+ window.Grain = {
286
+ GrainAnalytics,
287
+ createGrainAnalytics
288
+ };
289
+ }
290
+ return __toCommonJS(src_exports);
291
+ })();
292
+ //# sourceMappingURL=index.global.dev.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["/**\n * Grain Analytics Web SDK\n * A lightweight, dependency-free TypeScript SDK for sending analytics events to Grain's REST API\n */\n\nexport interface GrainEvent {\n eventName: string;\n userId?: string;\n properties?: Record<string, unknown>;\n timestamp?: Date;\n}\n\nexport interface EventRow {\n eventName: string;\n eventTs: string;\n userId: string;\n properties: Record<string, unknown>;\n eventDate: string;\n insertId: string;\n}\n\nexport type AuthStrategy = 'NONE' | 'SERVER_SIDE' | 'JWT';\n\nexport interface AuthProvider {\n getToken(): Promise<string> | string;\n}\n\nexport interface GrainConfig {\n tenantId: string;\n apiUrl?: string;\n authStrategy?: AuthStrategy;\n secretKey?: string; // For SERVER_SIDE auth\n authProvider?: AuthProvider; // For JWT auth\n batchSize?: number;\n flushInterval?: number; // milliseconds\n retryAttempts?: number;\n retryDelay?: number; // milliseconds\n debug?: boolean;\n}\n\nexport interface SendEventOptions {\n flush?: boolean; // Force immediate send\n}\n\n/**\n * Main Grain Analytics client\n */\ntype RequiredConfig = Required<Omit<GrainConfig, 'secretKey' | 'authProvider'>> & {\n secretKey?: string;\n authProvider?: AuthProvider;\n};\n\nexport class GrainAnalytics {\n private config: RequiredConfig;\n private eventQueue: EventRow[] = [];\n private flushTimer: number | null = null;\n private isDestroyed = false;\n\n constructor(config: GrainConfig) {\n this.config = {\n apiUrl: 'https://api.grainql.com',\n authStrategy: 'NONE',\n batchSize: 50,\n flushInterval: 5000, // 5 seconds\n retryAttempts: 3,\n retryDelay: 1000, // 1 second\n debug: false,\n ...config,\n tenantId: config.tenantId,\n };\n\n this.validateConfig();\n this.setupBeforeUnload();\n this.startFlushTimer();\n }\n\n private validateConfig(): void {\n if (!this.config.tenantId) {\n throw new Error('Grain Analytics: tenantId is required');\n }\n\n if (this.config.authStrategy === 'SERVER_SIDE' && !this.config.secretKey) {\n throw new Error('Grain Analytics: secretKey is required for SERVER_SIDE auth strategy');\n }\n\n if (this.config.authStrategy === 'JWT' && !this.config.authProvider) {\n throw new Error('Grain Analytics: authProvider is required for JWT auth strategy');\n }\n }\n\n private log(...args: unknown[]): void {\n if (this.config.debug) {\n console.log('[Grain Analytics]', ...args);\n }\n }\n\n private generateInsertId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n }\n\n private formatEvent(event: GrainEvent): EventRow {\n const timestamp = event.timestamp || new Date();\n const eventTs = timestamp.toISOString();\n const eventDate = timestamp.toISOString().split('T')[0];\n\n return {\n eventName: event.eventName,\n eventTs,\n userId: event.userId || 'anonymous',\n properties: event.properties || {},\n eventDate,\n insertId: this.generateInsertId(),\n };\n }\n\n private async getAuthHeaders(): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n switch (this.config.authStrategy) {\n case 'NONE':\n break;\n case 'SERVER_SIDE':\n headers['Authorization'] = `Chase ${this.config.secretKey}`;\n break;\n case 'JWT':\n if (this.config.authProvider) {\n const token = await this.config.authProvider.getToken();\n headers['Authorization'] = `Bearer ${token}`;\n }\n break;\n }\n\n return headers;\n }\n\n private async delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n private isRetriableError(error: unknown): boolean {\n if (error instanceof Error) {\n // Check for network errors or server errors\n if (error.message.includes('fetch')) return true;\n if (error.message.includes('network')) return true;\n }\n \n // Check for HTTP status codes that are retriable\n if (typeof error === 'object' && error !== null && 'status' in error) {\n const status = (error as { status: number }).status;\n return status >= 500 || status === 429; // Server errors or rate limiting\n }\n \n return false;\n }\n\n private async sendEvents(events: EventRow[]): Promise<void> {\n if (events.length === 0) return;\n\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {\n try {\n const headers = await this.getAuthHeaders();\n const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}`;\n\n this.log(`Sending ${events.length} events to ${url} (attempt ${attempt + 1})`);\n\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify({ events }),\n });\n\n if (!response.ok) {\n let errorMessage = `HTTP ${response.status}`;\n try {\n const errorBody = await response.json();\n if (errorBody?.message) {\n errorMessage = errorBody.message;\n }\n } catch {\n const errorText = await response.text();\n if (errorText) {\n errorMessage = errorText;\n }\n }\n \n const error = new Error(`Failed to send events: ${errorMessage}`) as Error & { status?: number };\n error.status = response.status;\n throw error;\n }\n\n this.log(`Successfully sent ${events.length} events`);\n return; // Success, exit retry loop\n \n } catch (error) {\n lastError = error;\n \n if (attempt === this.config.retryAttempts) {\n // Last attempt, don't retry\n break;\n }\n \n if (!this.isRetriableError(error)) {\n // Non-retriable error, don't retry\n break;\n }\n \n const delayMs = this.config.retryDelay * Math.pow(2, attempt); // Exponential backoff\n this.log(`Retrying in ${delayMs}ms after error:`, error);\n await this.delay(delayMs);\n }\n }\n\n console.error('[Grain Analytics] Failed to send events after all retries:', lastError);\n throw lastError;\n }\n\n private async sendEventsWithBeacon(events: EventRow[]): Promise<void> {\n if (events.length === 0) return;\n\n try {\n const headers = await this.getAuthHeaders();\n const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}`;\n\n const body = JSON.stringify({ events });\n\n // Try beacon API first (more reliable for page unload)\n if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {\n const blob = new Blob([body], { type: 'application/json' });\n const success = navigator.sendBeacon(url, blob);\n \n if (success) {\n this.log(`Successfully sent ${events.length} events via beacon`);\n return;\n }\n }\n\n // Fallback to fetch with keepalive\n await fetch(url, {\n method: 'POST',\n headers,\n body,\n keepalive: true,\n });\n\n this.log(`Successfully sent ${events.length} events via fetch (keepalive)`);\n } catch (error) {\n console.error('[Grain Analytics] Failed to send events via beacon:', error);\n }\n }\n\n private startFlushTimer(): void {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n }\n\n this.flushTimer = window.setInterval(() => {\n if (this.eventQueue.length > 0) {\n this.flush().catch((error) => {\n console.error('[Grain Analytics] Auto-flush failed:', error);\n });\n }\n }, this.config.flushInterval);\n }\n\n private setupBeforeUnload(): void {\n if (typeof window === 'undefined') return;\n\n const handleBeforeUnload = () => {\n if (this.eventQueue.length > 0) {\n // Use beacon API for reliable delivery during page unload\n this.sendEventsWithBeacon([...this.eventQueue]).catch(() => {\n // Silently fail - page is unloading\n });\n this.eventQueue = [];\n }\n };\n\n // Handle page unload\n window.addEventListener('beforeunload', handleBeforeUnload);\n window.addEventListener('pagehide', handleBeforeUnload);\n \n // Handle visibility change (page hidden)\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden' && this.eventQueue.length > 0) {\n this.sendEventsWithBeacon([...this.eventQueue]).catch(() => {\n // Silently fail\n });\n this.eventQueue = [];\n }\n });\n }\n\n /**\n * Track an analytics event\n */\n async track(eventName: string, properties?: Record<string, unknown>, options?: SendEventOptions): Promise<void>;\n async track(event: GrainEvent, options?: SendEventOptions): Promise<void>;\n async track(\n eventOrName: string | GrainEvent,\n propertiesOrOptions?: Record<string, unknown> | SendEventOptions,\n options?: SendEventOptions\n ): Promise<void> {\n if (this.isDestroyed) {\n throw new Error('Grain Analytics: Client has been destroyed');\n }\n\n let event: GrainEvent;\n let opts: SendEventOptions = {};\n\n if (typeof eventOrName === 'string') {\n event = {\n eventName: eventOrName,\n properties: propertiesOrOptions as Record<string, unknown>,\n };\n opts = options || {};\n } else {\n event = eventOrName;\n opts = propertiesOrOptions as SendEventOptions || {};\n }\n\n const formattedEvent = this.formatEvent(event);\n this.eventQueue.push(formattedEvent);\n\n this.log(`Queued event: ${event.eventName}`, event.properties);\n\n // Check if we should flush immediately\n if (opts.flush || this.eventQueue.length >= this.config.batchSize) {\n await this.flush();\n }\n }\n\n /**\n * Identify a user (sets userId for subsequent events)\n */\n identify(userId: string): void {\n // Store userId for future events - this would typically be handled\n // by the application layer, but we can provide a helper\n this.log(`Identified user: ${userId}`);\n }\n\n /**\n * Manually flush all queued events\n */\n async flush(): Promise<void> {\n if (this.eventQueue.length === 0) return;\n\n const eventsToSend = [...this.eventQueue];\n this.eventQueue = [];\n\n await this.sendEvents(eventsToSend);\n }\n\n /**\n * Destroy the client and clean up resources\n */\n destroy(): void {\n this.isDestroyed = true;\n \n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n\n // Send any remaining events\n if (this.eventQueue.length > 0) {\n this.sendEventsWithBeacon([...this.eventQueue]).catch(() => {\n // Silently fail during cleanup\n });\n this.eventQueue = [];\n }\n }\n}\n\n/**\n * Create a new Grain Analytics client\n */\nexport function createGrainAnalytics(config: GrainConfig): GrainAnalytics {\n return new GrainAnalytics(config);\n}\n\n// Default export for convenience\nexport default GrainAnalytics;\n\n// Global interface for IIFE build\ndeclare global {\n interface Window {\n Grain?: {\n GrainAnalytics: typeof GrainAnalytics;\n createGrainAnalytics: typeof createGrainAnalytics;\n };\n }\n}\n\n// Auto-setup for IIFE build\nif (typeof window !== 'undefined') {\n window.Grain = {\n GrainAnalytics,\n createGrainAnalytics,\n };\n}"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,3 @@
1
+ /* Grain Analytics Web SDK v1.0.0 | MIT License */
2
+ "use strict";var Grain=(()=>{var c=Object.defineProperty;var f=Object.getOwnPropertyDescriptor;var v=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var p=(s,e)=>{for(var t in e)c(s,t,{get:e[t],enumerable:!0})},y=(s,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of v(e))!g.call(s,n)&&n!==t&&c(s,n,{get:()=>e[n],enumerable:!(i=f(e,n))||i.enumerable});return s};var m=s=>y(c({},"__esModule",{value:!0}),s);var E={};p(E,{GrainAnalytics:()=>o,createGrainAnalytics:()=>l,default:()=>w});var o=class{constructor(e){this.eventQueue=[];this.flushTimer=null;this.isDestroyed=!1;this.config={apiUrl:"https://api.grainql.com",authStrategy:"NONE",batchSize:50,flushInterval:5e3,retryAttempts:3,retryDelay:1e3,debug:!1,...e,tenantId:e.tenantId},this.validateConfig(),this.setupBeforeUnload(),this.startFlushTimer()}validateConfig(){if(!this.config.tenantId)throw new Error("Grain Analytics: tenantId is required");if(this.config.authStrategy==="SERVER_SIDE"&&!this.config.secretKey)throw new Error("Grain Analytics: secretKey is required for SERVER_SIDE auth strategy");if(this.config.authStrategy==="JWT"&&!this.config.authProvider)throw new Error("Grain Analytics: authProvider is required for JWT auth strategy")}log(...e){this.config.debug&&console.log("[Grain Analytics]",...e)}generateInsertId(){return`${Date.now()}-${Math.random().toString(36).substr(2,9)}`}formatEvent(e){let t=e.timestamp||new Date,i=t.toISOString(),n=t.toISOString().split("T")[0];return{eventName:e.eventName,eventTs:i,userId:e.userId||"anonymous",properties:e.properties||{},eventDate:n,insertId:this.generateInsertId()}}async getAuthHeaders(){let e={"Content-Type":"application/json"};switch(this.config.authStrategy){case"NONE":break;case"SERVER_SIDE":e.Authorization=`Chase ${this.config.secretKey}`;break;case"JWT":if(this.config.authProvider){let t=await this.config.authProvider.getToken();e.Authorization=`Bearer ${t}`}break}return e}async delay(e){return new Promise(t=>setTimeout(t,e))}isRetriableError(e){if(e instanceof Error&&(e.message.includes("fetch")||e.message.includes("network")))return!0;if(typeof e=="object"&&e!==null&&"status"in e){let t=e.status;return t>=500||t===429}return!1}async sendEvents(e){if(e.length===0)return;let t;for(let i=0;i<=this.config.retryAttempts;i++)try{let n=await this.getAuthHeaders(),r=`${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}`;this.log(`Sending ${e.length} events to ${r} (attempt ${i+1})`);let a=await fetch(r,{method:"POST",headers:n,body:JSON.stringify({events:e})});if(!a.ok){let h=`HTTP ${a.status}`;try{let u=await a.json();u?.message&&(h=u.message)}catch{let u=await a.text();u&&(h=u)}let d=new Error(`Failed to send events: ${h}`);throw d.status=a.status,d}this.log(`Successfully sent ${e.length} events`);return}catch(n){if(t=n,i===this.config.retryAttempts||!this.isRetriableError(n))break;let r=this.config.retryDelay*Math.pow(2,i);this.log(`Retrying in ${r}ms after error:`,n),await this.delay(r)}throw console.error("[Grain Analytics] Failed to send events after all retries:",t),t}async sendEventsWithBeacon(e){if(e.length!==0)try{let t=await this.getAuthHeaders(),i=`${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}`,n=JSON.stringify({events:e});if(typeof navigator<"u"&&"sendBeacon"in navigator){let r=new Blob([n],{type:"application/json"});if(navigator.sendBeacon(i,r)){this.log(`Successfully sent ${e.length} events via beacon`);return}}await fetch(i,{method:"POST",headers:t,body:n,keepalive:!0}),this.log(`Successfully sent ${e.length} events via fetch (keepalive)`)}catch(t){console.error("[Grain Analytics] Failed to send events via beacon:",t)}}startFlushTimer(){this.flushTimer&&clearInterval(this.flushTimer),this.flushTimer=window.setInterval(()=>{this.eventQueue.length>0&&this.flush().catch(e=>{console.error("[Grain Analytics] Auto-flush failed:",e)})},this.config.flushInterval)}setupBeforeUnload(){if(typeof window>"u")return;let e=()=>{this.eventQueue.length>0&&(this.sendEventsWithBeacon([...this.eventQueue]).catch(()=>{}),this.eventQueue=[])};window.addEventListener("beforeunload",e),window.addEventListener("pagehide",e),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.eventQueue.length>0&&(this.sendEventsWithBeacon([...this.eventQueue]).catch(()=>{}),this.eventQueue=[])})}async track(e,t,i){if(this.isDestroyed)throw new Error("Grain Analytics: Client has been destroyed");let n,r={};typeof e=="string"?(n={eventName:e,properties:t},r=i||{}):(n=e,r=t||{});let a=this.formatEvent(n);this.eventQueue.push(a),this.log(`Queued event: ${n.eventName}`,n.properties),(r.flush||this.eventQueue.length>=this.config.batchSize)&&await this.flush()}identify(e){this.log(`Identified user: ${e}`)}async flush(){if(this.eventQueue.length===0)return;let e=[...this.eventQueue];this.eventQueue=[],await this.sendEvents(e)}destroy(){this.isDestroyed=!0,this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=null),this.eventQueue.length>0&&(this.sendEventsWithBeacon([...this.eventQueue]).catch(()=>{}),this.eventQueue=[])}};function l(s){return new o(s)}var w=o;typeof window<"u"&&(window.Grain={GrainAnalytics:o,createGrainAnalytics:l});return m(E);})();
3
+ //# sourceMappingURL=index.global.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["/**\n * Grain Analytics Web SDK\n * A lightweight, dependency-free TypeScript SDK for sending analytics events to Grain's REST API\n */\n\nexport interface GrainEvent {\n eventName: string;\n userId?: string;\n properties?: Record<string, unknown>;\n timestamp?: Date;\n}\n\nexport interface EventRow {\n eventName: string;\n eventTs: string;\n userId: string;\n properties: Record<string, unknown>;\n eventDate: string;\n insertId: string;\n}\n\nexport type AuthStrategy = 'NONE' | 'SERVER_SIDE' | 'JWT';\n\nexport interface AuthProvider {\n getToken(): Promise<string> | string;\n}\n\nexport interface GrainConfig {\n tenantId: string;\n apiUrl?: string;\n authStrategy?: AuthStrategy;\n secretKey?: string; // For SERVER_SIDE auth\n authProvider?: AuthProvider; // For JWT auth\n batchSize?: number;\n flushInterval?: number; // milliseconds\n retryAttempts?: number;\n retryDelay?: number; // milliseconds\n debug?: boolean;\n}\n\nexport interface SendEventOptions {\n flush?: boolean; // Force immediate send\n}\n\n/**\n * Main Grain Analytics client\n */\ntype RequiredConfig = Required<Omit<GrainConfig, 'secretKey' | 'authProvider'>> & {\n secretKey?: string;\n authProvider?: AuthProvider;\n};\n\nexport class GrainAnalytics {\n private config: RequiredConfig;\n private eventQueue: EventRow[] = [];\n private flushTimer: number | null = null;\n private isDestroyed = false;\n\n constructor(config: GrainConfig) {\n this.config = {\n apiUrl: 'https://api.grainql.com',\n authStrategy: 'NONE',\n batchSize: 50,\n flushInterval: 5000, // 5 seconds\n retryAttempts: 3,\n retryDelay: 1000, // 1 second\n debug: false,\n ...config,\n tenantId: config.tenantId,\n };\n\n this.validateConfig();\n this.setupBeforeUnload();\n this.startFlushTimer();\n }\n\n private validateConfig(): void {\n if (!this.config.tenantId) {\n throw new Error('Grain Analytics: tenantId is required');\n }\n\n if (this.config.authStrategy === 'SERVER_SIDE' && !this.config.secretKey) {\n throw new Error('Grain Analytics: secretKey is required for SERVER_SIDE auth strategy');\n }\n\n if (this.config.authStrategy === 'JWT' && !this.config.authProvider) {\n throw new Error('Grain Analytics: authProvider is required for JWT auth strategy');\n }\n }\n\n private log(...args: unknown[]): void {\n if (this.config.debug) {\n console.log('[Grain Analytics]', ...args);\n }\n }\n\n private generateInsertId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n }\n\n private formatEvent(event: GrainEvent): EventRow {\n const timestamp = event.timestamp || new Date();\n const eventTs = timestamp.toISOString();\n const eventDate = timestamp.toISOString().split('T')[0];\n\n return {\n eventName: event.eventName,\n eventTs,\n userId: event.userId || 'anonymous',\n properties: event.properties || {},\n eventDate,\n insertId: this.generateInsertId(),\n };\n }\n\n private async getAuthHeaders(): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n switch (this.config.authStrategy) {\n case 'NONE':\n break;\n case 'SERVER_SIDE':\n headers['Authorization'] = `Chase ${this.config.secretKey}`;\n break;\n case 'JWT':\n if (this.config.authProvider) {\n const token = await this.config.authProvider.getToken();\n headers['Authorization'] = `Bearer ${token}`;\n }\n break;\n }\n\n return headers;\n }\n\n private async delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n private isRetriableError(error: unknown): boolean {\n if (error instanceof Error) {\n // Check for network errors or server errors\n if (error.message.includes('fetch')) return true;\n if (error.message.includes('network')) return true;\n }\n \n // Check for HTTP status codes that are retriable\n if (typeof error === 'object' && error !== null && 'status' in error) {\n const status = (error as { status: number }).status;\n return status >= 500 || status === 429; // Server errors or rate limiting\n }\n \n return false;\n }\n\n private async sendEvents(events: EventRow[]): Promise<void> {\n if (events.length === 0) return;\n\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {\n try {\n const headers = await this.getAuthHeaders();\n const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}`;\n\n this.log(`Sending ${events.length} events to ${url} (attempt ${attempt + 1})`);\n\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify({ events }),\n });\n\n if (!response.ok) {\n let errorMessage = `HTTP ${response.status}`;\n try {\n const errorBody = await response.json();\n if (errorBody?.message) {\n errorMessage = errorBody.message;\n }\n } catch {\n const errorText = await response.text();\n if (errorText) {\n errorMessage = errorText;\n }\n }\n \n const error = new Error(`Failed to send events: ${errorMessage}`) as Error & { status?: number };\n error.status = response.status;\n throw error;\n }\n\n this.log(`Successfully sent ${events.length} events`);\n return; // Success, exit retry loop\n \n } catch (error) {\n lastError = error;\n \n if (attempt === this.config.retryAttempts) {\n // Last attempt, don't retry\n break;\n }\n \n if (!this.isRetriableError(error)) {\n // Non-retriable error, don't retry\n break;\n }\n \n const delayMs = this.config.retryDelay * Math.pow(2, attempt); // Exponential backoff\n this.log(`Retrying in ${delayMs}ms after error:`, error);\n await this.delay(delayMs);\n }\n }\n\n console.error('[Grain Analytics] Failed to send events after all retries:', lastError);\n throw lastError;\n }\n\n private async sendEventsWithBeacon(events: EventRow[]): Promise<void> {\n if (events.length === 0) return;\n\n try {\n const headers = await this.getAuthHeaders();\n const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}`;\n\n const body = JSON.stringify({ events });\n\n // Try beacon API first (more reliable for page unload)\n if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {\n const blob = new Blob([body], { type: 'application/json' });\n const success = navigator.sendBeacon(url, blob);\n \n if (success) {\n this.log(`Successfully sent ${events.length} events via beacon`);\n return;\n }\n }\n\n // Fallback to fetch with keepalive\n await fetch(url, {\n method: 'POST',\n headers,\n body,\n keepalive: true,\n });\n\n this.log(`Successfully sent ${events.length} events via fetch (keepalive)`);\n } catch (error) {\n console.error('[Grain Analytics] Failed to send events via beacon:', error);\n }\n }\n\n private startFlushTimer(): void {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n }\n\n this.flushTimer = window.setInterval(() => {\n if (this.eventQueue.length > 0) {\n this.flush().catch((error) => {\n console.error('[Grain Analytics] Auto-flush failed:', error);\n });\n }\n }, this.config.flushInterval);\n }\n\n private setupBeforeUnload(): void {\n if (typeof window === 'undefined') return;\n\n const handleBeforeUnload = () => {\n if (this.eventQueue.length > 0) {\n // Use beacon API for reliable delivery during page unload\n this.sendEventsWithBeacon([...this.eventQueue]).catch(() => {\n // Silently fail - page is unloading\n });\n this.eventQueue = [];\n }\n };\n\n // Handle page unload\n window.addEventListener('beforeunload', handleBeforeUnload);\n window.addEventListener('pagehide', handleBeforeUnload);\n \n // Handle visibility change (page hidden)\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'hidden' && this.eventQueue.length > 0) {\n this.sendEventsWithBeacon([...this.eventQueue]).catch(() => {\n // Silently fail\n });\n this.eventQueue = [];\n }\n });\n }\n\n /**\n * Track an analytics event\n */\n async track(eventName: string, properties?: Record<string, unknown>, options?: SendEventOptions): Promise<void>;\n async track(event: GrainEvent, options?: SendEventOptions): Promise<void>;\n async track(\n eventOrName: string | GrainEvent,\n propertiesOrOptions?: Record<string, unknown> | SendEventOptions,\n options?: SendEventOptions\n ): Promise<void> {\n if (this.isDestroyed) {\n throw new Error('Grain Analytics: Client has been destroyed');\n }\n\n let event: GrainEvent;\n let opts: SendEventOptions = {};\n\n if (typeof eventOrName === 'string') {\n event = {\n eventName: eventOrName,\n properties: propertiesOrOptions as Record<string, unknown>,\n };\n opts = options || {};\n } else {\n event = eventOrName;\n opts = propertiesOrOptions as SendEventOptions || {};\n }\n\n const formattedEvent = this.formatEvent(event);\n this.eventQueue.push(formattedEvent);\n\n this.log(`Queued event: ${event.eventName}`, event.properties);\n\n // Check if we should flush immediately\n if (opts.flush || this.eventQueue.length >= this.config.batchSize) {\n await this.flush();\n }\n }\n\n /**\n * Identify a user (sets userId for subsequent events)\n */\n identify(userId: string): void {\n // Store userId for future events - this would typically be handled\n // by the application layer, but we can provide a helper\n this.log(`Identified user: ${userId}`);\n }\n\n /**\n * Manually flush all queued events\n */\n async flush(): Promise<void> {\n if (this.eventQueue.length === 0) return;\n\n const eventsToSend = [...this.eventQueue];\n this.eventQueue = [];\n\n await this.sendEvents(eventsToSend);\n }\n\n /**\n * Destroy the client and clean up resources\n */\n destroy(): void {\n this.isDestroyed = true;\n \n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n\n // Send any remaining events\n if (this.eventQueue.length > 0) {\n this.sendEventsWithBeacon([...this.eventQueue]).catch(() => {\n // Silently fail during cleanup\n });\n this.eventQueue = [];\n }\n }\n}\n\n/**\n * Create a new Grain Analytics client\n */\nexport function createGrainAnalytics(config: GrainConfig): GrainAnalytics {\n return new GrainAnalytics(config);\n}\n\n// Default export for convenience\nexport default GrainAnalytics;\n\n// Global interface for IIFE build\ndeclare global {\n interface Window {\n Grain?: {\n GrainAnalytics: typeof GrainAnalytics;\n createGrainAnalytics: typeof createGrainAnalytics;\n };\n }\n}\n\n// Auto-setup for IIFE build\nif (typeof window !== 'undefined') {\n window.Grain = {\n GrainAnalytics,\n createGrainAnalytics,\n };\n}"],
5
+ "mappings": ";ybAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,oBAAAE,EAAA,yBAAAC,EAAA,YAAAC,IAoDO,IAAMF,EAAN,KAAqB,CAM1B,YAAYG,EAAqB,CAJjC,KAAQ,WAAyB,CAAC,EAClC,KAAQ,WAA4B,KACpC,KAAQ,YAAc,GAGpB,KAAK,OAAS,CACZ,OAAQ,0BACR,aAAc,OACd,UAAW,GACX,cAAe,IACf,cAAe,EACf,WAAY,IACZ,MAAO,GACP,GAAGA,EACH,SAAUA,EAAO,QACnB,EAEA,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,CACvB,CAEQ,gBAAuB,CAC7B,GAAI,CAAC,KAAK,OAAO,SACf,MAAM,IAAI,MAAM,uCAAuC,EAGzD,GAAI,KAAK,OAAO,eAAiB,eAAiB,CAAC,KAAK,OAAO,UAC7D,MAAM,IAAI,MAAM,sEAAsE,EAGxF,GAAI,KAAK,OAAO,eAAiB,OAAS,CAAC,KAAK,OAAO,aACrD,MAAM,IAAI,MAAM,iEAAiE,CAErF,CAEQ,OAAOC,EAAuB,CAChC,KAAK,OAAO,OACd,QAAQ,IAAI,oBAAqB,GAAGA,CAAI,CAE5C,CAEQ,kBAA2B,CACjC,MAAO,GAAG,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,OAAO,EAAG,CAAC,CAAC,EACjE,CAEQ,YAAYC,EAA6B,CAC/C,IAAMC,EAAYD,EAAM,WAAa,IAAI,KACnCE,EAAUD,EAAU,YAAY,EAChCE,EAAYF,EAAU,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,EAEtD,MAAO,CACL,UAAWD,EAAM,UACjB,QAAAE,EACA,OAAQF,EAAM,QAAU,YACxB,WAAYA,EAAM,YAAc,CAAC,EACjC,UAAAG,EACA,SAAU,KAAK,iBAAiB,CAClC,CACF,CAEA,MAAc,gBAAkD,CAC9D,IAAMC,EAAkC,CACtC,eAAgB,kBAClB,EAEA,OAAQ,KAAK,OAAO,aAAc,CAChC,IAAK,OACH,MACF,IAAK,cACHA,EAAQ,cAAmB,SAAS,KAAK,OAAO,SAAS,GACzD,MACF,IAAK,MACH,GAAI,KAAK,OAAO,aAAc,CAC5B,IAAMC,EAAQ,MAAM,KAAK,OAAO,aAAa,SAAS,EACtDD,EAAQ,cAAmB,UAAUC,CAAK,EAC5C,CACA,KACJ,CAEA,OAAOD,CACT,CAEA,MAAc,MAAME,EAA2B,CAC7C,OAAO,IAAI,QAAQC,GAAW,WAAWA,EAASD,CAAE,CAAC,CACvD,CAEQ,iBAAiBE,EAAyB,CAChD,GAAIA,aAAiB,QAEfA,EAAM,QAAQ,SAAS,OAAO,GAC9BA,EAAM,QAAQ,SAAS,SAAS,GAAG,MAAO,GAIhD,GAAI,OAAOA,GAAU,UAAYA,IAAU,MAAQ,WAAYA,EAAO,CACpE,IAAMC,EAAUD,EAA6B,OAC7C,OAAOC,GAAU,KAAOA,IAAW,GACrC,CAEA,MAAO,EACT,CAEA,MAAc,WAAWC,EAAmC,CAC1D,GAAIA,EAAO,SAAW,EAAG,OAEzB,IAAIC,EAEJ,QAASC,EAAU,EAAGA,GAAW,KAAK,OAAO,cAAeA,IAC1D,GAAI,CACF,IAAMR,EAAU,MAAM,KAAK,eAAe,EACpCS,EAAM,GAAG,KAAK,OAAO,MAAM,cAAc,mBAAmB,KAAK,OAAO,QAAQ,CAAC,GAEvF,KAAK,IAAI,WAAWH,EAAO,MAAM,cAAcG,CAAG,aAAaD,EAAU,CAAC,GAAG,EAE7E,IAAME,EAAW,MAAM,MAAMD,EAAK,CAChC,OAAQ,OACR,QAAAT,EACA,KAAM,KAAK,UAAU,CAAE,OAAAM,CAAO,CAAC,CACjC,CAAC,EAED,GAAI,CAACI,EAAS,GAAI,CAChB,IAAIC,EAAe,QAAQD,EAAS,MAAM,GAC1C,GAAI,CACF,IAAME,EAAY,MAAMF,EAAS,KAAK,EAClCE,GAAW,UACbD,EAAeC,EAAU,QAE7B,MAAQ,CACN,IAAMC,EAAY,MAAMH,EAAS,KAAK,EAClCG,IACFF,EAAeE,EAEnB,CAEA,IAAMT,EAAQ,IAAI,MAAM,0BAA0BO,CAAY,EAAE,EAChE,MAAAP,EAAM,OAASM,EAAS,OAClBN,CACR,CAEA,KAAK,IAAI,qBAAqBE,EAAO,MAAM,SAAS,EACpD,MAEF,OAASF,EAAO,CAQd,GAPAG,EAAYH,EAERI,IAAY,KAAK,OAAO,eAKxB,CAAC,KAAK,iBAAiBJ,CAAK,EAE9B,MAGF,IAAMU,EAAU,KAAK,OAAO,WAAa,KAAK,IAAI,EAAGN,CAAO,EAC5D,KAAK,IAAI,eAAeM,CAAO,kBAAmBV,CAAK,EACvD,MAAM,KAAK,MAAMU,CAAO,CAC1B,CAGF,cAAQ,MAAM,6DAA8DP,CAAS,EAC/EA,CACR,CAEA,MAAc,qBAAqBD,EAAmC,CACpE,GAAIA,EAAO,SAAW,EAEtB,GAAI,CACF,IAAMN,EAAU,MAAM,KAAK,eAAe,EACpCS,EAAM,GAAG,KAAK,OAAO,MAAM,cAAc,mBAAmB,KAAK,OAAO,QAAQ,CAAC,GAEjFM,EAAO,KAAK,UAAU,CAAE,OAAAT,CAAO,CAAC,EAGtC,GAAI,OAAO,UAAc,KAAe,eAAgB,UAAW,CACjE,IAAMU,EAAO,IAAI,KAAK,CAACD,CAAI,EAAG,CAAE,KAAM,kBAAmB,CAAC,EAG1D,GAFgB,UAAU,WAAWN,EAAKO,CAAI,EAEjC,CACX,KAAK,IAAI,qBAAqBV,EAAO,MAAM,oBAAoB,EAC/D,MACF,CACF,CAGA,MAAM,MAAMG,EAAK,CACf,OAAQ,OACR,QAAAT,EACA,KAAAe,EACA,UAAW,EACb,CAAC,EAED,KAAK,IAAI,qBAAqBT,EAAO,MAAM,+BAA+B,CAC5E,OAASF,EAAO,CACd,QAAQ,MAAM,sDAAuDA,CAAK,CAC5E,CACF,CAEQ,iBAAwB,CAC1B,KAAK,YACP,cAAc,KAAK,UAAU,EAG/B,KAAK,WAAa,OAAO,YAAY,IAAM,CACrC,KAAK,WAAW,OAAS,GAC3B,KAAK,MAAM,EAAE,MAAOA,GAAU,CAC5B,QAAQ,MAAM,uCAAwCA,CAAK,CAC7D,CAAC,CAEL,EAAG,KAAK,OAAO,aAAa,CAC9B,CAEQ,mBAA0B,CAChC,GAAI,OAAO,OAAW,IAAa,OAEnC,IAAMa,EAAqB,IAAM,CAC3B,KAAK,WAAW,OAAS,IAE3B,KAAK,qBAAqB,CAAC,GAAG,KAAK,UAAU,CAAC,EAAE,MAAM,IAAM,CAE5D,CAAC,EACD,KAAK,WAAa,CAAC,EAEvB,EAGA,OAAO,iBAAiB,eAAgBA,CAAkB,EAC1D,OAAO,iBAAiB,WAAYA,CAAkB,EAGtD,SAAS,iBAAiB,mBAAoB,IAAM,CAC9C,SAAS,kBAAoB,UAAY,KAAK,WAAW,OAAS,IACpE,KAAK,qBAAqB,CAAC,GAAG,KAAK,UAAU,CAAC,EAAE,MAAM,IAAM,CAE5D,CAAC,EACD,KAAK,WAAa,CAAC,EAEvB,CAAC,CACH,CAOA,MAAM,MACJC,EACAC,EACAC,EACe,CACf,GAAI,KAAK,YACP,MAAM,IAAI,MAAM,4CAA4C,EAG9D,IAAIxB,EACAyB,EAAyB,CAAC,EAE1B,OAAOH,GAAgB,UACzBtB,EAAQ,CACN,UAAWsB,EACX,WAAYC,CACd,EACAE,EAAOD,GAAW,CAAC,IAEnBxB,EAAQsB,EACRG,EAAOF,GAA2C,CAAC,GAGrD,IAAMG,EAAiB,KAAK,YAAY1B,CAAK,EAC7C,KAAK,WAAW,KAAK0B,CAAc,EAEnC,KAAK,IAAI,iBAAiB1B,EAAM,SAAS,GAAIA,EAAM,UAAU,GAGzDyB,EAAK,OAAS,KAAK,WAAW,QAAU,KAAK,OAAO,YACtD,MAAM,KAAK,MAAM,CAErB,CAKA,SAASE,EAAsB,CAG7B,KAAK,IAAI,oBAAoBA,CAAM,EAAE,CACvC,CAKA,MAAM,OAAuB,CAC3B,GAAI,KAAK,WAAW,SAAW,EAAG,OAElC,IAAMC,EAAe,CAAC,GAAG,KAAK,UAAU,EACxC,KAAK,WAAa,CAAC,EAEnB,MAAM,KAAK,WAAWA,CAAY,CACpC,CAKA,SAAgB,CACd,KAAK,YAAc,GAEf,KAAK,aACP,cAAc,KAAK,UAAU,EAC7B,KAAK,WAAa,MAIhB,KAAK,WAAW,OAAS,IAC3B,KAAK,qBAAqB,CAAC,GAAG,KAAK,UAAU,CAAC,EAAE,MAAM,IAAM,CAE5D,CAAC,EACD,KAAK,WAAa,CAAC,EAEvB,CACF,EAKO,SAAShC,EAAqBE,EAAqC,CACxE,OAAO,IAAIH,EAAeG,CAAM,CAClC,CAGA,IAAOD,EAAQF,EAaX,OAAO,OAAW,MACpB,OAAO,MAAQ,CACb,eAAAA,EACA,qBAAAC,CACF",
6
+ "names": ["src_exports", "__export", "GrainAnalytics", "createGrainAnalytics", "src_default", "config", "args", "event", "timestamp", "eventTs", "eventDate", "headers", "token", "ms", "resolve", "error", "status", "events", "lastError", "attempt", "url", "response", "errorMessage", "errorBody", "errorText", "delayMs", "body", "blob", "handleBeforeUnload", "eventOrName", "propertiesOrOptions", "options", "opts", "formattedEvent", "userId", "eventsToSend"]
7
+ }