krisspy-sdk 0.4.0 → 0.5.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.
package/dist/index.d.mts CHANGED
@@ -26,6 +26,20 @@ interface KrisspyClientOptions {
26
26
  * When false, uses legacy /data/* endpoints which require backend owner auth.
27
27
  */
28
28
  useRLS?: boolean;
29
+ /**
30
+ * Custom storage adapter for session persistence.
31
+ * Use this in React Native with AsyncStorage or MMKV.
32
+ * Falls back to localStorage (browser) or in-memory storage.
33
+ *
34
+ * @example
35
+ * import AsyncStorage from '@react-native-async-storage/async-storage'
36
+ * const krisspy = createClient({ ..., storage: AsyncStorage })
37
+ */
38
+ storage?: {
39
+ getItem(key: string): string | null | Promise<string | null>;
40
+ setItem(key: string, value: string): void | Promise<void>;
41
+ removeItem(key: string): void | Promise<void>;
42
+ };
29
43
  }
30
44
  interface User {
31
45
  id: string;
@@ -184,8 +198,24 @@ declare class HttpClient {
184
198
  delete<T>(path: string, params?: Record<string, any>): Promise<HttpResponse<T>>;
185
199
  }
186
200
 
201
+ /**
202
+ * Platform detection and cross-platform utilities
203
+ * Safe to evaluate in any JS environment (browser, Node, React Native, Expo Snack)
204
+ */
205
+ /** Storage adapter interface - allows custom storage (AsyncStorage, MMKV, etc.) */
206
+ interface StorageAdapter {
207
+ getItem(key: string): string | null | Promise<string | null>;
208
+ setItem(key: string, value: string): void | Promise<void>;
209
+ removeItem(key: string): void | Promise<void>;
210
+ }
211
+ /** Check if running in a browser with DOM APIs */
212
+ declare function isBrowser(): boolean;
213
+ /** Check if running in React Native */
214
+ declare function isReactNative(): boolean;
215
+
187
216
  /**
188
217
  * Auth Module - User authentication for Krisspy backends
218
+ * Compatible with Browser, React Native, and Node.js
189
219
  */
190
220
 
191
221
  declare class KrisspyAuth {
@@ -195,11 +225,13 @@ declare class KrisspyAuth {
195
225
  private currentUser;
196
226
  private listeners;
197
227
  private refreshInterval?;
198
- constructor(http: HttpClient, backendId: string);
228
+ private storage;
229
+ constructor(http: HttpClient, backendId: string, storage?: StorageAdapter);
199
230
  /**
200
231
  * Get current session from storage
201
232
  */
202
233
  private restoreSession;
234
+ private hydrateSession;
203
235
  /**
204
236
  * Store session
205
237
  */
@@ -299,6 +331,7 @@ declare class KrisspyAuth {
299
331
  };
300
332
  /**
301
333
  * Set session from external source (e.g., OAuth callback)
334
+ * Browser-only: parses tokens from URL hash
302
335
  */
303
336
  setSessionFromUrl(): Promise<AuthResponse>;
304
337
  }
@@ -699,6 +732,80 @@ declare class KrisspyRealtime {
699
732
  private log;
700
733
  }
701
734
 
735
+ /**
736
+ * Krisspy Analytics - Lightweight event tracking module
737
+ *
738
+ * Tracks page views, sessions, custom events. Sends batched events
739
+ * to the analytics ingest endpoint. Works with SPAs (intercepts pushState).
740
+ * Browser-only module - safely no-ops in React Native / Node.js.
741
+ *
742
+ * @example
743
+ * const krisspy = createClient({ backendId: '...', anonKey: '...' })
744
+ * krisspy.analytics.init({ autoTrackPageViews: true })
745
+ * krisspy.analytics.track('button_click', { label: 'signup' })
746
+ */
747
+
748
+ interface AnalyticsInitOptions {
749
+ /** Auto-track page views on init + navigation (default: true) */
750
+ autoTrackPageViews?: boolean;
751
+ /** Auto-track SPA navigation via pushState/popstate (default: true) */
752
+ autoTrackNavigation?: boolean;
753
+ /** Flush interval in ms (default: 5000) */
754
+ flushInterval?: number;
755
+ /** Enable debug logging (default: false) */
756
+ debug?: boolean;
757
+ }
758
+ interface AnalyticsEvent {
759
+ eventName: string;
760
+ sessionId: string;
761
+ userId?: string;
762
+ properties?: Record<string, any>;
763
+ timestamp: string;
764
+ }
765
+ declare class KrisspyAnalytics {
766
+ private http;
767
+ private backendId;
768
+ private queue;
769
+ private sessionId;
770
+ private userId;
771
+ private userTraits;
772
+ private initialized;
773
+ private flushTimer;
774
+ private options;
775
+ private originalPushState;
776
+ private popstateHandler;
777
+ private unloadHandler;
778
+ constructor(http: HttpClient, backendId: string);
779
+ /**
780
+ * Initialize analytics tracking
781
+ * No-ops in non-browser environments (React Native, Node.js)
782
+ */
783
+ init(options?: AnalyticsInitOptions): void;
784
+ /**
785
+ * Track a custom event
786
+ */
787
+ track(eventName: string, properties?: Record<string, any>): void;
788
+ /**
789
+ * Identify a user (attach userId + traits to all future events)
790
+ */
791
+ identify(userId: string, traits?: Record<string, any>): void;
792
+ /**
793
+ * Flush event queue to the server (async)
794
+ */
795
+ flush(): Promise<void>;
796
+ /**
797
+ * Synchronous flush using sendBeacon (for page unload)
798
+ */
799
+ private flushSync;
800
+ /**
801
+ * Stop tracking and cleanup all listeners/timers
802
+ */
803
+ destroy(): void;
804
+ private getOrCreateSessionId;
805
+ private getIngestUrl;
806
+ private setupNavigationTracking;
807
+ }
808
+
702
809
  /**
703
810
  * Query Builder - Supabase-style fluent API for database queries
704
811
  */
@@ -850,6 +957,7 @@ declare class KrisspyClient {
850
957
  private _auth;
851
958
  private _storage;
852
959
  private _realtime;
960
+ private _analytics;
853
961
  private useRLS;
854
962
  private debug;
855
963
  constructor(options: KrisspyClientOptions);
@@ -898,6 +1006,23 @@ declare class KrisspyClient {
898
1006
  * const { error } = await krisspy.storage.from('uploads').remove(['old.jpg'])
899
1007
  */
900
1008
  get storage(): KrisspyStorage;
1009
+ /**
1010
+ * Analytics module for event tracking
1011
+ *
1012
+ * @example
1013
+ * // Initialize tracking
1014
+ * krisspy.analytics.init()
1015
+ *
1016
+ * // Track custom events
1017
+ * krisspy.analytics.track('button_click', { label: 'signup' })
1018
+ *
1019
+ * // Identify a user
1020
+ * krisspy.analytics.identify('user_123', { plan: 'pro' })
1021
+ *
1022
+ * // Cleanup
1023
+ * krisspy.analytics.destroy()
1024
+ */
1025
+ get analytics(): KrisspyAnalytics;
901
1026
  /**
902
1027
  * Create a realtime channel for subscribing to database changes
903
1028
  *
@@ -1081,4 +1206,4 @@ declare class KrisspyClient {
1081
1206
  */
1082
1207
  declare function createClient(options: KrisspyClientOptions): KrisspyClient;
1083
1208
 
1084
- export { type AuthChangeEvent, type AuthResponse, type ChannelState, type FileObject, type FileUploadOptions, type Filter, type FilterOperator, type FunctionInvokeOptions, type FunctionResponse, HttpClient, KrisspyAuth, KrisspyClient, type KrisspyClientOptions, type KrisspyError, KrisspyRealtime, KrisspyStorage, type MutationResponse, type OAuthProvider, type OrderBy, type PostgresChangesConfig, QueryBuilder, type QueryOptions, type QueryResponse, RealtimeChannel, type RealtimeEvent, type RealtimePayload, type Session, type SignInCredentials, type SignUpCredentials, type SingleResponse, StorageBucket, type UploadAndLinkOptions, type UploadAndLinkResponse, type User, createClient };
1209
+ export { type AnalyticsEvent, type AnalyticsInitOptions, type AuthChangeEvent, type AuthResponse, type ChannelState, type FileObject, type FileUploadOptions, type Filter, type FilterOperator, type FunctionInvokeOptions, type FunctionResponse, HttpClient, KrisspyAnalytics, KrisspyAuth, KrisspyClient, type KrisspyClientOptions, type KrisspyError, KrisspyRealtime, KrisspyStorage, type MutationResponse, type OAuthProvider, type OrderBy, type PostgresChangesConfig, QueryBuilder, type QueryOptions, type QueryResponse, RealtimeChannel, type RealtimeEvent, type RealtimePayload, type Session, type SignInCredentials, type SignUpCredentials, type SingleResponse, type StorageAdapter, StorageBucket, type UploadAndLinkOptions, type UploadAndLinkResponse, type User, createClient, isBrowser, isReactNative };
package/dist/index.d.ts CHANGED
@@ -26,6 +26,20 @@ interface KrisspyClientOptions {
26
26
  * When false, uses legacy /data/* endpoints which require backend owner auth.
27
27
  */
28
28
  useRLS?: boolean;
29
+ /**
30
+ * Custom storage adapter for session persistence.
31
+ * Use this in React Native with AsyncStorage or MMKV.
32
+ * Falls back to localStorage (browser) or in-memory storage.
33
+ *
34
+ * @example
35
+ * import AsyncStorage from '@react-native-async-storage/async-storage'
36
+ * const krisspy = createClient({ ..., storage: AsyncStorage })
37
+ */
38
+ storage?: {
39
+ getItem(key: string): string | null | Promise<string | null>;
40
+ setItem(key: string, value: string): void | Promise<void>;
41
+ removeItem(key: string): void | Promise<void>;
42
+ };
29
43
  }
30
44
  interface User {
31
45
  id: string;
@@ -184,8 +198,24 @@ declare class HttpClient {
184
198
  delete<T>(path: string, params?: Record<string, any>): Promise<HttpResponse<T>>;
185
199
  }
186
200
 
201
+ /**
202
+ * Platform detection and cross-platform utilities
203
+ * Safe to evaluate in any JS environment (browser, Node, React Native, Expo Snack)
204
+ */
205
+ /** Storage adapter interface - allows custom storage (AsyncStorage, MMKV, etc.) */
206
+ interface StorageAdapter {
207
+ getItem(key: string): string | null | Promise<string | null>;
208
+ setItem(key: string, value: string): void | Promise<void>;
209
+ removeItem(key: string): void | Promise<void>;
210
+ }
211
+ /** Check if running in a browser with DOM APIs */
212
+ declare function isBrowser(): boolean;
213
+ /** Check if running in React Native */
214
+ declare function isReactNative(): boolean;
215
+
187
216
  /**
188
217
  * Auth Module - User authentication for Krisspy backends
218
+ * Compatible with Browser, React Native, and Node.js
189
219
  */
190
220
 
191
221
  declare class KrisspyAuth {
@@ -195,11 +225,13 @@ declare class KrisspyAuth {
195
225
  private currentUser;
196
226
  private listeners;
197
227
  private refreshInterval?;
198
- constructor(http: HttpClient, backendId: string);
228
+ private storage;
229
+ constructor(http: HttpClient, backendId: string, storage?: StorageAdapter);
199
230
  /**
200
231
  * Get current session from storage
201
232
  */
202
233
  private restoreSession;
234
+ private hydrateSession;
203
235
  /**
204
236
  * Store session
205
237
  */
@@ -299,6 +331,7 @@ declare class KrisspyAuth {
299
331
  };
300
332
  /**
301
333
  * Set session from external source (e.g., OAuth callback)
334
+ * Browser-only: parses tokens from URL hash
302
335
  */
303
336
  setSessionFromUrl(): Promise<AuthResponse>;
304
337
  }
@@ -699,6 +732,80 @@ declare class KrisspyRealtime {
699
732
  private log;
700
733
  }
701
734
 
735
+ /**
736
+ * Krisspy Analytics - Lightweight event tracking module
737
+ *
738
+ * Tracks page views, sessions, custom events. Sends batched events
739
+ * to the analytics ingest endpoint. Works with SPAs (intercepts pushState).
740
+ * Browser-only module - safely no-ops in React Native / Node.js.
741
+ *
742
+ * @example
743
+ * const krisspy = createClient({ backendId: '...', anonKey: '...' })
744
+ * krisspy.analytics.init({ autoTrackPageViews: true })
745
+ * krisspy.analytics.track('button_click', { label: 'signup' })
746
+ */
747
+
748
+ interface AnalyticsInitOptions {
749
+ /** Auto-track page views on init + navigation (default: true) */
750
+ autoTrackPageViews?: boolean;
751
+ /** Auto-track SPA navigation via pushState/popstate (default: true) */
752
+ autoTrackNavigation?: boolean;
753
+ /** Flush interval in ms (default: 5000) */
754
+ flushInterval?: number;
755
+ /** Enable debug logging (default: false) */
756
+ debug?: boolean;
757
+ }
758
+ interface AnalyticsEvent {
759
+ eventName: string;
760
+ sessionId: string;
761
+ userId?: string;
762
+ properties?: Record<string, any>;
763
+ timestamp: string;
764
+ }
765
+ declare class KrisspyAnalytics {
766
+ private http;
767
+ private backendId;
768
+ private queue;
769
+ private sessionId;
770
+ private userId;
771
+ private userTraits;
772
+ private initialized;
773
+ private flushTimer;
774
+ private options;
775
+ private originalPushState;
776
+ private popstateHandler;
777
+ private unloadHandler;
778
+ constructor(http: HttpClient, backendId: string);
779
+ /**
780
+ * Initialize analytics tracking
781
+ * No-ops in non-browser environments (React Native, Node.js)
782
+ */
783
+ init(options?: AnalyticsInitOptions): void;
784
+ /**
785
+ * Track a custom event
786
+ */
787
+ track(eventName: string, properties?: Record<string, any>): void;
788
+ /**
789
+ * Identify a user (attach userId + traits to all future events)
790
+ */
791
+ identify(userId: string, traits?: Record<string, any>): void;
792
+ /**
793
+ * Flush event queue to the server (async)
794
+ */
795
+ flush(): Promise<void>;
796
+ /**
797
+ * Synchronous flush using sendBeacon (for page unload)
798
+ */
799
+ private flushSync;
800
+ /**
801
+ * Stop tracking and cleanup all listeners/timers
802
+ */
803
+ destroy(): void;
804
+ private getOrCreateSessionId;
805
+ private getIngestUrl;
806
+ private setupNavigationTracking;
807
+ }
808
+
702
809
  /**
703
810
  * Query Builder - Supabase-style fluent API for database queries
704
811
  */
@@ -850,6 +957,7 @@ declare class KrisspyClient {
850
957
  private _auth;
851
958
  private _storage;
852
959
  private _realtime;
960
+ private _analytics;
853
961
  private useRLS;
854
962
  private debug;
855
963
  constructor(options: KrisspyClientOptions);
@@ -898,6 +1006,23 @@ declare class KrisspyClient {
898
1006
  * const { error } = await krisspy.storage.from('uploads').remove(['old.jpg'])
899
1007
  */
900
1008
  get storage(): KrisspyStorage;
1009
+ /**
1010
+ * Analytics module for event tracking
1011
+ *
1012
+ * @example
1013
+ * // Initialize tracking
1014
+ * krisspy.analytics.init()
1015
+ *
1016
+ * // Track custom events
1017
+ * krisspy.analytics.track('button_click', { label: 'signup' })
1018
+ *
1019
+ * // Identify a user
1020
+ * krisspy.analytics.identify('user_123', { plan: 'pro' })
1021
+ *
1022
+ * // Cleanup
1023
+ * krisspy.analytics.destroy()
1024
+ */
1025
+ get analytics(): KrisspyAnalytics;
901
1026
  /**
902
1027
  * Create a realtime channel for subscribing to database changes
903
1028
  *
@@ -1081,4 +1206,4 @@ declare class KrisspyClient {
1081
1206
  */
1082
1207
  declare function createClient(options: KrisspyClientOptions): KrisspyClient;
1083
1208
 
1084
- export { type AuthChangeEvent, type AuthResponse, type ChannelState, type FileObject, type FileUploadOptions, type Filter, type FilterOperator, type FunctionInvokeOptions, type FunctionResponse, HttpClient, KrisspyAuth, KrisspyClient, type KrisspyClientOptions, type KrisspyError, KrisspyRealtime, KrisspyStorage, type MutationResponse, type OAuthProvider, type OrderBy, type PostgresChangesConfig, QueryBuilder, type QueryOptions, type QueryResponse, RealtimeChannel, type RealtimeEvent, type RealtimePayload, type Session, type SignInCredentials, type SignUpCredentials, type SingleResponse, StorageBucket, type UploadAndLinkOptions, type UploadAndLinkResponse, type User, createClient };
1209
+ export { type AnalyticsEvent, type AnalyticsInitOptions, type AuthChangeEvent, type AuthResponse, type ChannelState, type FileObject, type FileUploadOptions, type Filter, type FilterOperator, type FunctionInvokeOptions, type FunctionResponse, HttpClient, KrisspyAnalytics, KrisspyAuth, KrisspyClient, type KrisspyClientOptions, type KrisspyError, KrisspyRealtime, KrisspyStorage, type MutationResponse, type OAuthProvider, type OrderBy, type PostgresChangesConfig, QueryBuilder, type QueryOptions, type QueryResponse, RealtimeChannel, type RealtimeEvent, type RealtimePayload, type Session, type SignInCredentials, type SignUpCredentials, type SingleResponse, type StorageAdapter, StorageBucket, type UploadAndLinkOptions, type UploadAndLinkResponse, type User, createClient, isBrowser, isReactNative };
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  HttpClient: () => HttpClient,
24
+ KrisspyAnalytics: () => KrisspyAnalytics,
24
25
  KrisspyAuth: () => KrisspyAuth,
25
26
  KrisspyClient: () => KrisspyClient,
26
27
  KrisspyRealtime: () => KrisspyRealtime,
@@ -28,7 +29,9 @@ __export(index_exports, {
28
29
  QueryBuilder: () => QueryBuilder,
29
30
  RealtimeChannel: () => RealtimeChannel,
30
31
  StorageBucket: () => StorageBucket,
31
- createClient: () => createClient
32
+ createClient: () => createClient,
33
+ isBrowser: () => isBrowser,
34
+ isReactNative: () => isReactNative
32
35
  });
33
36
  module.exports = __toCommonJS(index_exports);
34
37
 
@@ -161,43 +164,136 @@ var HttpClient = class {
161
164
  }
162
165
  };
163
166
 
167
+ // src/platform.ts
168
+ function isBrowser() {
169
+ return typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement === "function";
170
+ }
171
+ function isReactNative() {
172
+ return typeof navigator !== "undefined" && typeof navigator.product === "string" && navigator.product === "ReactNative";
173
+ }
174
+ function getLocalStorage() {
175
+ if (!isBrowser()) return null;
176
+ try {
177
+ const testKey = "__krisspy_test__";
178
+ window.localStorage.setItem(testKey, "1");
179
+ window.localStorage.removeItem(testKey);
180
+ return window.localStorage;
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+ var MemoryStorage = class {
186
+ constructor() {
187
+ this.store = /* @__PURE__ */ new Map();
188
+ }
189
+ getItem(key) {
190
+ return this.store.get(key) ?? null;
191
+ }
192
+ setItem(key, value) {
193
+ this.store.set(key, value);
194
+ }
195
+ removeItem(key) {
196
+ this.store.delete(key);
197
+ }
198
+ };
199
+ function resolveStorage(custom) {
200
+ if (custom) return custom;
201
+ return getLocalStorage() ?? new MemoryStorage();
202
+ }
203
+ function base64Encode(input) {
204
+ if (typeof btoa === "function") {
205
+ return btoa(input);
206
+ }
207
+ if (typeof Buffer !== "undefined") {
208
+ return Buffer.from(input, "binary").toString("base64");
209
+ }
210
+ return manualBase64Encode(input);
211
+ }
212
+ function base64Decode(input) {
213
+ if (typeof atob === "function") {
214
+ return atob(input);
215
+ }
216
+ if (typeof Buffer !== "undefined") {
217
+ return Buffer.from(input, "base64").toString("binary");
218
+ }
219
+ return manualBase64Decode(input);
220
+ }
221
+ var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
222
+ function manualBase64Encode(input) {
223
+ let output = "";
224
+ for (let i = 0; i < input.length; i += 3) {
225
+ const a = input.charCodeAt(i);
226
+ const b = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
227
+ const c = i + 2 < input.length ? input.charCodeAt(i + 2) : 0;
228
+ output += CHARS[a >> 2];
229
+ output += CHARS[(a & 3) << 4 | b >> 4];
230
+ output += i + 1 < input.length ? CHARS[(b & 15) << 2 | c >> 6] : "=";
231
+ output += i + 2 < input.length ? CHARS[c & 63] : "=";
232
+ }
233
+ return output;
234
+ }
235
+ function manualBase64Decode(input) {
236
+ let output = "";
237
+ const str = input.replace(/=+$/, "");
238
+ for (let i = 0; i < str.length; i += 4) {
239
+ const a = CHARS.indexOf(str[i]);
240
+ const b = CHARS.indexOf(str[i + 1]);
241
+ const c = CHARS.indexOf(str[i + 2]);
242
+ const d = CHARS.indexOf(str[i + 3]);
243
+ output += String.fromCharCode(a << 2 | b >> 4);
244
+ if (c !== -1) output += String.fromCharCode((b & 15) << 4 | c >> 2);
245
+ if (d !== -1) output += String.fromCharCode((c & 3) << 6 | d);
246
+ }
247
+ return output;
248
+ }
249
+
164
250
  // src/auth.ts
165
251
  var STORAGE_KEY = "krisspy-auth-session";
166
252
  var KrisspyAuth = class {
167
- constructor(http, backendId) {
253
+ constructor(http, backendId, storage) {
168
254
  this.currentSession = null;
169
255
  this.currentUser = null;
170
256
  this.listeners = [];
171
257
  this.http = http;
172
258
  this.backendId = backendId;
259
+ this.storage = resolveStorage(storage);
173
260
  this.restoreSession();
174
261
  }
175
262
  /**
176
263
  * Get current session from storage
177
264
  */
178
265
  restoreSession() {
179
- if (typeof window === "undefined") return;
180
266
  try {
181
- const stored = localStorage.getItem(`${STORAGE_KEY}-${this.backendId}`);
182
- if (stored) {
183
- const session = JSON.parse(stored);
184
- if (session.expires_at && Date.now() > session.expires_at * 1e3) {
185
- this.clearSession();
186
- return;
187
- }
188
- this.setSession(session);
267
+ const result = this.storage.getItem(`${STORAGE_KEY}-${this.backendId}`);
268
+ if (result instanceof Promise) {
269
+ result.then((stored) => {
270
+ if (stored) this.hydrateSession(stored);
271
+ }).catch(() => {
272
+ });
273
+ } else if (result) {
274
+ this.hydrateSession(result);
189
275
  }
190
276
  } catch (err) {
191
277
  console.warn("[Krisspy Auth] Failed to restore session:", err);
192
278
  }
193
279
  }
280
+ hydrateSession(stored) {
281
+ try {
282
+ const session = JSON.parse(stored);
283
+ if (session.expires_at && Date.now() > session.expires_at * 1e3) {
284
+ this.clearSession();
285
+ return;
286
+ }
287
+ this.setSession(session);
288
+ } catch {
289
+ }
290
+ }
194
291
  /**
195
292
  * Store session
196
293
  */
197
294
  saveSession(session) {
198
- if (typeof window === "undefined") return;
199
295
  try {
200
- localStorage.setItem(`${STORAGE_KEY}-${this.backendId}`, JSON.stringify(session));
296
+ this.storage.setItem(`${STORAGE_KEY}-${this.backendId}`, JSON.stringify(session));
201
297
  } catch (err) {
202
298
  console.warn("[Krisspy Auth] Failed to save session:", err);
203
299
  }
@@ -213,8 +309,9 @@ var KrisspyAuth = class {
213
309
  clearInterval(this.refreshInterval);
214
310
  this.refreshInterval = void 0;
215
311
  }
216
- if (typeof window !== "undefined") {
217
- localStorage.removeItem(`${STORAGE_KEY}-${this.backendId}`);
312
+ try {
313
+ this.storage.removeItem(`${STORAGE_KEY}-${this.backendId}`);
314
+ } catch {
218
315
  }
219
316
  }
220
317
  /**
@@ -319,7 +416,7 @@ var KrisspyAuth = class {
319
416
  if (response.error) {
320
417
  return { data: null, error: response.error };
321
418
  }
322
- if (typeof window !== "undefined" && response.data?.url) {
419
+ if (isBrowser() && response.data?.url) {
323
420
  window.location.href = response.data.url;
324
421
  }
325
422
  return { data: response.data, error: null };
@@ -442,9 +539,10 @@ var KrisspyAuth = class {
442
539
  }
443
540
  /**
444
541
  * Set session from external source (e.g., OAuth callback)
542
+ * Browser-only: parses tokens from URL hash
445
543
  */
446
544
  async setSessionFromUrl() {
447
- if (typeof window === "undefined") {
545
+ if (!isBrowser()) {
448
546
  return { data: { user: null, session: null }, error: { message: "Not in browser" } };
449
547
  }
450
548
  const hash = window.location.hash.substring(1);
@@ -455,7 +553,7 @@ var KrisspyAuth = class {
455
553
  return { data: { user: null, session: null }, error: { message: "No access token in URL" } };
456
554
  }
457
555
  try {
458
- const payload = JSON.parse(atob(accessToken.split(".")[1]));
556
+ const payload = JSON.parse(base64Decode(accessToken.split(".")[1]));
459
557
  const session = {
460
558
  access_token: accessToken,
461
559
  refresh_token: refreshToken ?? void 0,
@@ -763,27 +861,31 @@ var StorageBucket = class {
763
861
  };
764
862
  }
765
863
  }
766
- // Helper: Convert Blob to base64
767
- blobToBase64(blob) {
768
- return new Promise((resolve, reject) => {
769
- const reader = new FileReader();
770
- reader.onloadend = () => {
771
- const result = reader.result;
772
- const base64 = result.includes(",") ? result.split(",")[1] : result;
773
- resolve(base64);
774
- };
775
- reader.onerror = reject;
776
- reader.readAsDataURL(blob);
777
- });
864
+ // Helper: Convert Blob to base64 (cross-platform)
865
+ async blobToBase64(blob) {
866
+ if (typeof FileReader !== "undefined") {
867
+ return new Promise((resolve, reject) => {
868
+ const reader = new FileReader();
869
+ reader.onloadend = () => {
870
+ const result = reader.result;
871
+ const base64 = result.includes(",") ? result.split(",")[1] : result;
872
+ resolve(base64);
873
+ };
874
+ reader.onerror = reject;
875
+ reader.readAsDataURL(blob);
876
+ });
877
+ }
878
+ const buffer = await blob.arrayBuffer();
879
+ return this.arrayBufferToBase64(buffer);
778
880
  }
779
- // Helper: Convert ArrayBuffer to base64
881
+ // Helper: Convert ArrayBuffer to base64 (cross-platform)
780
882
  arrayBufferToBase64(buffer) {
781
- let binary = "";
782
883
  const bytes = new Uint8Array(buffer);
884
+ let binary = "";
783
885
  for (let i = 0; i < bytes.length; i++) {
784
886
  binary += String.fromCharCode(bytes[i]);
785
887
  }
786
- return btoa(binary);
888
+ return base64Encode(binary);
787
889
  }
788
890
  // Helper: Detect MIME type from file extension
789
891
  detectMimeType(path) {
@@ -1144,6 +1246,188 @@ var KrisspyRealtime = class {
1144
1246
  }
1145
1247
  };
1146
1248
 
1249
+ // src/analytics.ts
1250
+ var KrisspyAnalytics = class {
1251
+ constructor(http, backendId) {
1252
+ this.queue = [];
1253
+ this.sessionId = null;
1254
+ this.userId = null;
1255
+ this.userTraits = {};
1256
+ this.initialized = false;
1257
+ this.flushTimer = null;
1258
+ this.options = {};
1259
+ this.originalPushState = null;
1260
+ this.popstateHandler = null;
1261
+ this.unloadHandler = null;
1262
+ this.http = http;
1263
+ this.backendId = backendId;
1264
+ }
1265
+ /**
1266
+ * Initialize analytics tracking
1267
+ * No-ops in non-browser environments (React Native, Node.js)
1268
+ */
1269
+ init(options) {
1270
+ if (this.initialized) return;
1271
+ if (!isBrowser()) return;
1272
+ this.options = {
1273
+ autoTrackPageViews: true,
1274
+ autoTrackNavigation: true,
1275
+ flushInterval: 5e3,
1276
+ debug: false,
1277
+ ...options
1278
+ };
1279
+ this.sessionId = this.getOrCreateSessionId();
1280
+ this.initialized = true;
1281
+ if (this.options.autoTrackPageViews) {
1282
+ this.track("session_start", {});
1283
+ this.track("page_view", { url: window.location.href, referrer: document.referrer });
1284
+ }
1285
+ if (this.options.autoTrackNavigation) {
1286
+ this.setupNavigationTracking();
1287
+ }
1288
+ this.unloadHandler = () => {
1289
+ this.track("session_end", {});
1290
+ this.flushSync();
1291
+ };
1292
+ window.addEventListener("beforeunload", this.unloadHandler);
1293
+ this.flushTimer = setInterval(() => this.flush(), this.options.flushInterval);
1294
+ if (this.options.debug) {
1295
+ console.log("[Krisspy Analytics] Initialized", { backendId: this.backendId, sessionId: this.sessionId });
1296
+ }
1297
+ }
1298
+ /**
1299
+ * Track a custom event
1300
+ */
1301
+ track(eventName, properties) {
1302
+ if (!this.initialized) return;
1303
+ const event = {
1304
+ eventName,
1305
+ sessionId: this.sessionId,
1306
+ userId: this.userId || void 0,
1307
+ properties: {
1308
+ ...properties,
1309
+ ...this.userId ? { _user_traits: this.userTraits } : {}
1310
+ },
1311
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1312
+ };
1313
+ this.queue.push(event);
1314
+ if (this.options.debug) {
1315
+ console.log("[Krisspy Analytics] Tracked:", eventName, properties);
1316
+ }
1317
+ }
1318
+ /**
1319
+ * Identify a user (attach userId + traits to all future events)
1320
+ */
1321
+ identify(userId, traits) {
1322
+ this.userId = userId;
1323
+ this.userTraits = traits || {};
1324
+ if (this.initialized) {
1325
+ this.track("identify", { userId, ...traits });
1326
+ }
1327
+ }
1328
+ /**
1329
+ * Flush event queue to the server (async)
1330
+ */
1331
+ async flush() {
1332
+ if (!this.queue.length) return;
1333
+ const events = this.queue.splice(0, 50);
1334
+ const payload = { appId: this.backendId, events };
1335
+ try {
1336
+ await this.http.post("/api/v1/analytics/events", payload);
1337
+ if (this.options.debug) {
1338
+ console.log(`[Krisspy Analytics] Flushed ${events.length} events`);
1339
+ }
1340
+ } catch {
1341
+ this.queue.unshift(...events);
1342
+ }
1343
+ }
1344
+ /**
1345
+ * Synchronous flush using sendBeacon (for page unload)
1346
+ */
1347
+ flushSync() {
1348
+ if (!this.queue.length) return;
1349
+ if (!isBrowser()) return;
1350
+ const events = this.queue.splice(0, 50);
1351
+ const payload = JSON.stringify({ appId: this.backendId, events });
1352
+ try {
1353
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
1354
+ const blob = new Blob([payload], { type: "application/json" });
1355
+ navigator.sendBeacon(this.getIngestUrl(), blob);
1356
+ } else {
1357
+ fetch(this.getIngestUrl(), {
1358
+ method: "POST",
1359
+ headers: { "Content-Type": "application/json" },
1360
+ body: payload,
1361
+ keepalive: true
1362
+ }).catch(() => {
1363
+ });
1364
+ }
1365
+ } catch {
1366
+ }
1367
+ }
1368
+ /**
1369
+ * Stop tracking and cleanup all listeners/timers
1370
+ */
1371
+ destroy() {
1372
+ if (!this.initialized) return;
1373
+ this.flushSync();
1374
+ if (this.flushTimer) {
1375
+ clearInterval(this.flushTimer);
1376
+ this.flushTimer = null;
1377
+ }
1378
+ if (isBrowser()) {
1379
+ if (this.originalPushState) {
1380
+ history.pushState = this.originalPushState;
1381
+ this.originalPushState = null;
1382
+ }
1383
+ if (this.popstateHandler) {
1384
+ window.removeEventListener("popstate", this.popstateHandler);
1385
+ this.popstateHandler = null;
1386
+ }
1387
+ if (this.unloadHandler) {
1388
+ window.removeEventListener("beforeunload", this.unloadHandler);
1389
+ this.unloadHandler = null;
1390
+ }
1391
+ }
1392
+ this.initialized = false;
1393
+ this.queue = [];
1394
+ if (this.options.debug) {
1395
+ console.log("[Krisspy Analytics] Destroyed");
1396
+ }
1397
+ }
1398
+ // -- Private helpers --------------------------------------------------------
1399
+ getOrCreateSessionId() {
1400
+ const key = "_ka_sid";
1401
+ if (isBrowser()) {
1402
+ try {
1403
+ const existing = window.localStorage.getItem(key);
1404
+ if (existing) return existing;
1405
+ const id = Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
1406
+ window.localStorage.setItem(key, id);
1407
+ return id;
1408
+ } catch {
1409
+ }
1410
+ }
1411
+ return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
1412
+ }
1413
+ getIngestUrl() {
1414
+ return `${this.http.baseUrl}/api/v1/analytics/events`;
1415
+ }
1416
+ setupNavigationTracking() {
1417
+ if (!isBrowser()) return;
1418
+ this.originalPushState = history.pushState.bind(history);
1419
+ const self = this;
1420
+ history.pushState = function(...args) {
1421
+ self.originalPushState(...args);
1422
+ self.track("page_view", { url: window.location.href });
1423
+ };
1424
+ this.popstateHandler = () => {
1425
+ this.track("page_view", { url: window.location.href });
1426
+ };
1427
+ window.addEventListener("popstate", this.popstateHandler);
1428
+ }
1429
+ };
1430
+
1147
1431
  // src/query-builder.ts
1148
1432
  var QueryBuilder = class {
1149
1433
  constructor(http, backendId, tableName, useRLS = true) {
@@ -1435,9 +1719,10 @@ var KrisspyClient = class {
1435
1719
  },
1436
1720
  debug: options.debug
1437
1721
  });
1438
- this._auth = new KrisspyAuth(this.http, this.backendId);
1722
+ this._auth = new KrisspyAuth(this.http, this.backendId, options.storage);
1439
1723
  this._storage = new KrisspyStorage(this.http, this.backendId);
1440
1724
  this._realtime = new KrisspyRealtime(this.baseUrl, this.backendId, this.debug);
1725
+ this._analytics = new KrisspyAnalytics(this.http, this.backendId);
1441
1726
  this._auth.onAuthStateChange((event) => {
1442
1727
  if (event === "SIGNED_IN") {
1443
1728
  const session = this._auth.session();
@@ -1498,6 +1783,25 @@ var KrisspyClient = class {
1498
1783
  get storage() {
1499
1784
  return this._storage;
1500
1785
  }
1786
+ /**
1787
+ * Analytics module for event tracking
1788
+ *
1789
+ * @example
1790
+ * // Initialize tracking
1791
+ * krisspy.analytics.init()
1792
+ *
1793
+ * // Track custom events
1794
+ * krisspy.analytics.track('button_click', { label: 'signup' })
1795
+ *
1796
+ * // Identify a user
1797
+ * krisspy.analytics.identify('user_123', { plan: 'pro' })
1798
+ *
1799
+ * // Cleanup
1800
+ * krisspy.analytics.destroy()
1801
+ */
1802
+ get analytics() {
1803
+ return this._analytics;
1804
+ }
1501
1805
  /**
1502
1806
  * Create a realtime channel for subscribing to database changes
1503
1807
  *
@@ -1666,6 +1970,7 @@ function createClient(options) {
1666
1970
  // Annotate the CommonJS export names for ESM import in node:
1667
1971
  0 && (module.exports = {
1668
1972
  HttpClient,
1973
+ KrisspyAnalytics,
1669
1974
  KrisspyAuth,
1670
1975
  KrisspyClient,
1671
1976
  KrisspyRealtime,
@@ -1673,5 +1978,7 @@ function createClient(options) {
1673
1978
  QueryBuilder,
1674
1979
  RealtimeChannel,
1675
1980
  StorageBucket,
1676
- createClient
1981
+ createClient,
1982
+ isBrowser,
1983
+ isReactNative
1677
1984
  });
package/dist/index.mjs CHANGED
@@ -127,43 +127,136 @@ var HttpClient = class {
127
127
  }
128
128
  };
129
129
 
130
+ // src/platform.ts
131
+ function isBrowser() {
132
+ return typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement === "function";
133
+ }
134
+ function isReactNative() {
135
+ return typeof navigator !== "undefined" && typeof navigator.product === "string" && navigator.product === "ReactNative";
136
+ }
137
+ function getLocalStorage() {
138
+ if (!isBrowser()) return null;
139
+ try {
140
+ const testKey = "__krisspy_test__";
141
+ window.localStorage.setItem(testKey, "1");
142
+ window.localStorage.removeItem(testKey);
143
+ return window.localStorage;
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+ var MemoryStorage = class {
149
+ constructor() {
150
+ this.store = /* @__PURE__ */ new Map();
151
+ }
152
+ getItem(key) {
153
+ return this.store.get(key) ?? null;
154
+ }
155
+ setItem(key, value) {
156
+ this.store.set(key, value);
157
+ }
158
+ removeItem(key) {
159
+ this.store.delete(key);
160
+ }
161
+ };
162
+ function resolveStorage(custom) {
163
+ if (custom) return custom;
164
+ return getLocalStorage() ?? new MemoryStorage();
165
+ }
166
+ function base64Encode(input) {
167
+ if (typeof btoa === "function") {
168
+ return btoa(input);
169
+ }
170
+ if (typeof Buffer !== "undefined") {
171
+ return Buffer.from(input, "binary").toString("base64");
172
+ }
173
+ return manualBase64Encode(input);
174
+ }
175
+ function base64Decode(input) {
176
+ if (typeof atob === "function") {
177
+ return atob(input);
178
+ }
179
+ if (typeof Buffer !== "undefined") {
180
+ return Buffer.from(input, "base64").toString("binary");
181
+ }
182
+ return manualBase64Decode(input);
183
+ }
184
+ var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
185
+ function manualBase64Encode(input) {
186
+ let output = "";
187
+ for (let i = 0; i < input.length; i += 3) {
188
+ const a = input.charCodeAt(i);
189
+ const b = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
190
+ const c = i + 2 < input.length ? input.charCodeAt(i + 2) : 0;
191
+ output += CHARS[a >> 2];
192
+ output += CHARS[(a & 3) << 4 | b >> 4];
193
+ output += i + 1 < input.length ? CHARS[(b & 15) << 2 | c >> 6] : "=";
194
+ output += i + 2 < input.length ? CHARS[c & 63] : "=";
195
+ }
196
+ return output;
197
+ }
198
+ function manualBase64Decode(input) {
199
+ let output = "";
200
+ const str = input.replace(/=+$/, "");
201
+ for (let i = 0; i < str.length; i += 4) {
202
+ const a = CHARS.indexOf(str[i]);
203
+ const b = CHARS.indexOf(str[i + 1]);
204
+ const c = CHARS.indexOf(str[i + 2]);
205
+ const d = CHARS.indexOf(str[i + 3]);
206
+ output += String.fromCharCode(a << 2 | b >> 4);
207
+ if (c !== -1) output += String.fromCharCode((b & 15) << 4 | c >> 2);
208
+ if (d !== -1) output += String.fromCharCode((c & 3) << 6 | d);
209
+ }
210
+ return output;
211
+ }
212
+
130
213
  // src/auth.ts
131
214
  var STORAGE_KEY = "krisspy-auth-session";
132
215
  var KrisspyAuth = class {
133
- constructor(http, backendId) {
216
+ constructor(http, backendId, storage) {
134
217
  this.currentSession = null;
135
218
  this.currentUser = null;
136
219
  this.listeners = [];
137
220
  this.http = http;
138
221
  this.backendId = backendId;
222
+ this.storage = resolveStorage(storage);
139
223
  this.restoreSession();
140
224
  }
141
225
  /**
142
226
  * Get current session from storage
143
227
  */
144
228
  restoreSession() {
145
- if (typeof window === "undefined") return;
146
229
  try {
147
- const stored = localStorage.getItem(`${STORAGE_KEY}-${this.backendId}`);
148
- if (stored) {
149
- const session = JSON.parse(stored);
150
- if (session.expires_at && Date.now() > session.expires_at * 1e3) {
151
- this.clearSession();
152
- return;
153
- }
154
- this.setSession(session);
230
+ const result = this.storage.getItem(`${STORAGE_KEY}-${this.backendId}`);
231
+ if (result instanceof Promise) {
232
+ result.then((stored) => {
233
+ if (stored) this.hydrateSession(stored);
234
+ }).catch(() => {
235
+ });
236
+ } else if (result) {
237
+ this.hydrateSession(result);
155
238
  }
156
239
  } catch (err) {
157
240
  console.warn("[Krisspy Auth] Failed to restore session:", err);
158
241
  }
159
242
  }
243
+ hydrateSession(stored) {
244
+ try {
245
+ const session = JSON.parse(stored);
246
+ if (session.expires_at && Date.now() > session.expires_at * 1e3) {
247
+ this.clearSession();
248
+ return;
249
+ }
250
+ this.setSession(session);
251
+ } catch {
252
+ }
253
+ }
160
254
  /**
161
255
  * Store session
162
256
  */
163
257
  saveSession(session) {
164
- if (typeof window === "undefined") return;
165
258
  try {
166
- localStorage.setItem(`${STORAGE_KEY}-${this.backendId}`, JSON.stringify(session));
259
+ this.storage.setItem(`${STORAGE_KEY}-${this.backendId}`, JSON.stringify(session));
167
260
  } catch (err) {
168
261
  console.warn("[Krisspy Auth] Failed to save session:", err);
169
262
  }
@@ -179,8 +272,9 @@ var KrisspyAuth = class {
179
272
  clearInterval(this.refreshInterval);
180
273
  this.refreshInterval = void 0;
181
274
  }
182
- if (typeof window !== "undefined") {
183
- localStorage.removeItem(`${STORAGE_KEY}-${this.backendId}`);
275
+ try {
276
+ this.storage.removeItem(`${STORAGE_KEY}-${this.backendId}`);
277
+ } catch {
184
278
  }
185
279
  }
186
280
  /**
@@ -285,7 +379,7 @@ var KrisspyAuth = class {
285
379
  if (response.error) {
286
380
  return { data: null, error: response.error };
287
381
  }
288
- if (typeof window !== "undefined" && response.data?.url) {
382
+ if (isBrowser() && response.data?.url) {
289
383
  window.location.href = response.data.url;
290
384
  }
291
385
  return { data: response.data, error: null };
@@ -408,9 +502,10 @@ var KrisspyAuth = class {
408
502
  }
409
503
  /**
410
504
  * Set session from external source (e.g., OAuth callback)
505
+ * Browser-only: parses tokens from URL hash
411
506
  */
412
507
  async setSessionFromUrl() {
413
- if (typeof window === "undefined") {
508
+ if (!isBrowser()) {
414
509
  return { data: { user: null, session: null }, error: { message: "Not in browser" } };
415
510
  }
416
511
  const hash = window.location.hash.substring(1);
@@ -421,7 +516,7 @@ var KrisspyAuth = class {
421
516
  return { data: { user: null, session: null }, error: { message: "No access token in URL" } };
422
517
  }
423
518
  try {
424
- const payload = JSON.parse(atob(accessToken.split(".")[1]));
519
+ const payload = JSON.parse(base64Decode(accessToken.split(".")[1]));
425
520
  const session = {
426
521
  access_token: accessToken,
427
522
  refresh_token: refreshToken ?? void 0,
@@ -729,27 +824,31 @@ var StorageBucket = class {
729
824
  };
730
825
  }
731
826
  }
732
- // Helper: Convert Blob to base64
733
- blobToBase64(blob) {
734
- return new Promise((resolve, reject) => {
735
- const reader = new FileReader();
736
- reader.onloadend = () => {
737
- const result = reader.result;
738
- const base64 = result.includes(",") ? result.split(",")[1] : result;
739
- resolve(base64);
740
- };
741
- reader.onerror = reject;
742
- reader.readAsDataURL(blob);
743
- });
827
+ // Helper: Convert Blob to base64 (cross-platform)
828
+ async blobToBase64(blob) {
829
+ if (typeof FileReader !== "undefined") {
830
+ return new Promise((resolve, reject) => {
831
+ const reader = new FileReader();
832
+ reader.onloadend = () => {
833
+ const result = reader.result;
834
+ const base64 = result.includes(",") ? result.split(",")[1] : result;
835
+ resolve(base64);
836
+ };
837
+ reader.onerror = reject;
838
+ reader.readAsDataURL(blob);
839
+ });
840
+ }
841
+ const buffer = await blob.arrayBuffer();
842
+ return this.arrayBufferToBase64(buffer);
744
843
  }
745
- // Helper: Convert ArrayBuffer to base64
844
+ // Helper: Convert ArrayBuffer to base64 (cross-platform)
746
845
  arrayBufferToBase64(buffer) {
747
- let binary = "";
748
846
  const bytes = new Uint8Array(buffer);
847
+ let binary = "";
749
848
  for (let i = 0; i < bytes.length; i++) {
750
849
  binary += String.fromCharCode(bytes[i]);
751
850
  }
752
- return btoa(binary);
851
+ return base64Encode(binary);
753
852
  }
754
853
  // Helper: Detect MIME type from file extension
755
854
  detectMimeType(path) {
@@ -1110,6 +1209,188 @@ var KrisspyRealtime = class {
1110
1209
  }
1111
1210
  };
1112
1211
 
1212
+ // src/analytics.ts
1213
+ var KrisspyAnalytics = class {
1214
+ constructor(http, backendId) {
1215
+ this.queue = [];
1216
+ this.sessionId = null;
1217
+ this.userId = null;
1218
+ this.userTraits = {};
1219
+ this.initialized = false;
1220
+ this.flushTimer = null;
1221
+ this.options = {};
1222
+ this.originalPushState = null;
1223
+ this.popstateHandler = null;
1224
+ this.unloadHandler = null;
1225
+ this.http = http;
1226
+ this.backendId = backendId;
1227
+ }
1228
+ /**
1229
+ * Initialize analytics tracking
1230
+ * No-ops in non-browser environments (React Native, Node.js)
1231
+ */
1232
+ init(options) {
1233
+ if (this.initialized) return;
1234
+ if (!isBrowser()) return;
1235
+ this.options = {
1236
+ autoTrackPageViews: true,
1237
+ autoTrackNavigation: true,
1238
+ flushInterval: 5e3,
1239
+ debug: false,
1240
+ ...options
1241
+ };
1242
+ this.sessionId = this.getOrCreateSessionId();
1243
+ this.initialized = true;
1244
+ if (this.options.autoTrackPageViews) {
1245
+ this.track("session_start", {});
1246
+ this.track("page_view", { url: window.location.href, referrer: document.referrer });
1247
+ }
1248
+ if (this.options.autoTrackNavigation) {
1249
+ this.setupNavigationTracking();
1250
+ }
1251
+ this.unloadHandler = () => {
1252
+ this.track("session_end", {});
1253
+ this.flushSync();
1254
+ };
1255
+ window.addEventListener("beforeunload", this.unloadHandler);
1256
+ this.flushTimer = setInterval(() => this.flush(), this.options.flushInterval);
1257
+ if (this.options.debug) {
1258
+ console.log("[Krisspy Analytics] Initialized", { backendId: this.backendId, sessionId: this.sessionId });
1259
+ }
1260
+ }
1261
+ /**
1262
+ * Track a custom event
1263
+ */
1264
+ track(eventName, properties) {
1265
+ if (!this.initialized) return;
1266
+ const event = {
1267
+ eventName,
1268
+ sessionId: this.sessionId,
1269
+ userId: this.userId || void 0,
1270
+ properties: {
1271
+ ...properties,
1272
+ ...this.userId ? { _user_traits: this.userTraits } : {}
1273
+ },
1274
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1275
+ };
1276
+ this.queue.push(event);
1277
+ if (this.options.debug) {
1278
+ console.log("[Krisspy Analytics] Tracked:", eventName, properties);
1279
+ }
1280
+ }
1281
+ /**
1282
+ * Identify a user (attach userId + traits to all future events)
1283
+ */
1284
+ identify(userId, traits) {
1285
+ this.userId = userId;
1286
+ this.userTraits = traits || {};
1287
+ if (this.initialized) {
1288
+ this.track("identify", { userId, ...traits });
1289
+ }
1290
+ }
1291
+ /**
1292
+ * Flush event queue to the server (async)
1293
+ */
1294
+ async flush() {
1295
+ if (!this.queue.length) return;
1296
+ const events = this.queue.splice(0, 50);
1297
+ const payload = { appId: this.backendId, events };
1298
+ try {
1299
+ await this.http.post("/api/v1/analytics/events", payload);
1300
+ if (this.options.debug) {
1301
+ console.log(`[Krisspy Analytics] Flushed ${events.length} events`);
1302
+ }
1303
+ } catch {
1304
+ this.queue.unshift(...events);
1305
+ }
1306
+ }
1307
+ /**
1308
+ * Synchronous flush using sendBeacon (for page unload)
1309
+ */
1310
+ flushSync() {
1311
+ if (!this.queue.length) return;
1312
+ if (!isBrowser()) return;
1313
+ const events = this.queue.splice(0, 50);
1314
+ const payload = JSON.stringify({ appId: this.backendId, events });
1315
+ try {
1316
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
1317
+ const blob = new Blob([payload], { type: "application/json" });
1318
+ navigator.sendBeacon(this.getIngestUrl(), blob);
1319
+ } else {
1320
+ fetch(this.getIngestUrl(), {
1321
+ method: "POST",
1322
+ headers: { "Content-Type": "application/json" },
1323
+ body: payload,
1324
+ keepalive: true
1325
+ }).catch(() => {
1326
+ });
1327
+ }
1328
+ } catch {
1329
+ }
1330
+ }
1331
+ /**
1332
+ * Stop tracking and cleanup all listeners/timers
1333
+ */
1334
+ destroy() {
1335
+ if (!this.initialized) return;
1336
+ this.flushSync();
1337
+ if (this.flushTimer) {
1338
+ clearInterval(this.flushTimer);
1339
+ this.flushTimer = null;
1340
+ }
1341
+ if (isBrowser()) {
1342
+ if (this.originalPushState) {
1343
+ history.pushState = this.originalPushState;
1344
+ this.originalPushState = null;
1345
+ }
1346
+ if (this.popstateHandler) {
1347
+ window.removeEventListener("popstate", this.popstateHandler);
1348
+ this.popstateHandler = null;
1349
+ }
1350
+ if (this.unloadHandler) {
1351
+ window.removeEventListener("beforeunload", this.unloadHandler);
1352
+ this.unloadHandler = null;
1353
+ }
1354
+ }
1355
+ this.initialized = false;
1356
+ this.queue = [];
1357
+ if (this.options.debug) {
1358
+ console.log("[Krisspy Analytics] Destroyed");
1359
+ }
1360
+ }
1361
+ // -- Private helpers --------------------------------------------------------
1362
+ getOrCreateSessionId() {
1363
+ const key = "_ka_sid";
1364
+ if (isBrowser()) {
1365
+ try {
1366
+ const existing = window.localStorage.getItem(key);
1367
+ if (existing) return existing;
1368
+ const id = Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
1369
+ window.localStorage.setItem(key, id);
1370
+ return id;
1371
+ } catch {
1372
+ }
1373
+ }
1374
+ return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
1375
+ }
1376
+ getIngestUrl() {
1377
+ return `${this.http.baseUrl}/api/v1/analytics/events`;
1378
+ }
1379
+ setupNavigationTracking() {
1380
+ if (!isBrowser()) return;
1381
+ this.originalPushState = history.pushState.bind(history);
1382
+ const self = this;
1383
+ history.pushState = function(...args) {
1384
+ self.originalPushState(...args);
1385
+ self.track("page_view", { url: window.location.href });
1386
+ };
1387
+ this.popstateHandler = () => {
1388
+ this.track("page_view", { url: window.location.href });
1389
+ };
1390
+ window.addEventListener("popstate", this.popstateHandler);
1391
+ }
1392
+ };
1393
+
1113
1394
  // src/query-builder.ts
1114
1395
  var QueryBuilder = class {
1115
1396
  constructor(http, backendId, tableName, useRLS = true) {
@@ -1401,9 +1682,10 @@ var KrisspyClient = class {
1401
1682
  },
1402
1683
  debug: options.debug
1403
1684
  });
1404
- this._auth = new KrisspyAuth(this.http, this.backendId);
1685
+ this._auth = new KrisspyAuth(this.http, this.backendId, options.storage);
1405
1686
  this._storage = new KrisspyStorage(this.http, this.backendId);
1406
1687
  this._realtime = new KrisspyRealtime(this.baseUrl, this.backendId, this.debug);
1688
+ this._analytics = new KrisspyAnalytics(this.http, this.backendId);
1407
1689
  this._auth.onAuthStateChange((event) => {
1408
1690
  if (event === "SIGNED_IN") {
1409
1691
  const session = this._auth.session();
@@ -1464,6 +1746,25 @@ var KrisspyClient = class {
1464
1746
  get storage() {
1465
1747
  return this._storage;
1466
1748
  }
1749
+ /**
1750
+ * Analytics module for event tracking
1751
+ *
1752
+ * @example
1753
+ * // Initialize tracking
1754
+ * krisspy.analytics.init()
1755
+ *
1756
+ * // Track custom events
1757
+ * krisspy.analytics.track('button_click', { label: 'signup' })
1758
+ *
1759
+ * // Identify a user
1760
+ * krisspy.analytics.identify('user_123', { plan: 'pro' })
1761
+ *
1762
+ * // Cleanup
1763
+ * krisspy.analytics.destroy()
1764
+ */
1765
+ get analytics() {
1766
+ return this._analytics;
1767
+ }
1467
1768
  /**
1468
1769
  * Create a realtime channel for subscribing to database changes
1469
1770
  *
@@ -1631,6 +1932,7 @@ function createClient(options) {
1631
1932
  }
1632
1933
  export {
1633
1934
  HttpClient,
1935
+ KrisspyAnalytics,
1634
1936
  KrisspyAuth,
1635
1937
  KrisspyClient,
1636
1938
  KrisspyRealtime,
@@ -1638,5 +1940,7 @@ export {
1638
1940
  QueryBuilder,
1639
1941
  RealtimeChannel,
1640
1942
  StorageBucket,
1641
- createClient
1943
+ createClient,
1944
+ isBrowser,
1945
+ isReactNative
1642
1946
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "krisspy-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Krisspy Cloud SDK - Database, Auth, Storage, and Functions for your apps",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",