@clianta/sdk 1.3.0 → 1.4.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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.3.0
2
+ * Clianta SDK v1.4.0
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -12,7 +12,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
12
12
  * @see SDK_VERSION in core/config.ts
13
13
  */
14
14
  /** SDK Version */
15
- const SDK_VERSION = '1.3.0';
15
+ const SDK_VERSION = '1.4.0';
16
16
  /** Default API endpoint based on environment */
17
17
  const getDefaultApiEndpoint = () => {
18
18
  if (typeof window === 'undefined')
@@ -38,6 +38,7 @@ const DEFAULT_CONFIG = {
38
38
  projectId: '',
39
39
  apiEndpoint: getDefaultApiEndpoint(),
40
40
  authToken: '',
41
+ apiKey: '',
41
42
  debug: false,
42
43
  autoPageView: true,
43
44
  plugins: DEFAULT_PLUGINS,
@@ -185,12 +186,39 @@ class Transport {
185
186
  return this.send(url, payload);
186
187
  }
187
188
  /**
188
- * Send identify request
189
+ * Send identify request.
190
+ * Returns contactId from the server response so the Tracker can store it.
189
191
  */
190
192
  async sendIdentify(data) {
191
193
  const url = `${this.config.apiEndpoint}/api/public/track/identify`;
192
- const payload = JSON.stringify(data);
193
- return this.send(url, payload);
194
+ try {
195
+ const response = await this.fetchWithTimeout(url, {
196
+ method: 'POST',
197
+ headers: { 'Content-Type': 'application/json' },
198
+ body: JSON.stringify(data),
199
+ keepalive: true,
200
+ });
201
+ const body = await response.json().catch(() => ({}));
202
+ if (response.ok) {
203
+ logger.debug('Identify successful, contactId:', body.contactId);
204
+ return {
205
+ success: true,
206
+ status: response.status,
207
+ contactId: body.contactId ?? undefined,
208
+ };
209
+ }
210
+ if (response.status >= 500) {
211
+ logger.warn(`Identify server error (${response.status})`);
212
+ }
213
+ else {
214
+ logger.error(`Identify failed with status ${response.status}:`, body.message);
215
+ }
216
+ return { success: false, status: response.status };
217
+ }
218
+ catch (error) {
219
+ logger.error('Identify request failed:', error);
220
+ return { success: false, error: error };
221
+ }
194
222
  }
195
223
  /**
196
224
  * Send events synchronously (for page unload)
@@ -2365,453 +2393,129 @@ class ConsentManager {
2365
2393
  }
2366
2394
 
2367
2395
  /**
2368
- * Clianta SDK - Main Tracker Class
2369
- * @see SDK_VERSION in core/config.ts
2396
+ * Clianta SDK - Event Triggers Manager
2397
+ * Manages event-driven automation and email notifications
2370
2398
  */
2371
2399
  /**
2372
- * Main Clianta Tracker Class
2400
+ * Event Triggers Manager
2401
+ * Handles event-driven automation based on CRM actions
2402
+ *
2403
+ * Similar to:
2404
+ * - Salesforce: Process Builder, Flow Automation
2405
+ * - HubSpot: Workflows, Email Sequences
2406
+ * - Pipedrive: Workflow Automation
2373
2407
  */
2374
- class Tracker {
2375
- constructor(workspaceId, userConfig = {}) {
2376
- this.plugins = [];
2377
- this.isInitialized = false;
2378
- /** Pending identify retry on next flush */
2379
- this.pendingIdentify = null;
2380
- if (!workspaceId) {
2381
- throw new Error('[Clianta] Workspace ID is required');
2382
- }
2408
+ class EventTriggersManager {
2409
+ constructor(apiEndpoint, workspaceId, authToken) {
2410
+ this.triggers = new Map();
2411
+ this.listeners = new Map();
2412
+ this.apiEndpoint = apiEndpoint;
2383
2413
  this.workspaceId = workspaceId;
2384
- this.config = mergeConfig(userConfig);
2385
- // Setup debug mode
2386
- logger.enabled = this.config.debug;
2387
- logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
2388
- // Initialize consent manager
2389
- this.consentManager = new ConsentManager({
2390
- ...this.config.consent,
2391
- onConsentChange: (state, previous) => {
2392
- this.onConsentChange(state, previous);
2393
- },
2394
- });
2395
- // Initialize transport and queue
2396
- this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
2397
- this.queue = new EventQueue(this.transport, {
2398
- batchSize: this.config.batchSize,
2399
- flushInterval: this.config.flushInterval,
2400
- });
2401
- // Get or create visitor and session IDs based on mode
2402
- this.visitorId = this.createVisitorId();
2403
- this.sessionId = this.createSessionId();
2404
- logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
2405
- // Initialize plugins
2406
- this.initPlugins();
2407
- this.isInitialized = true;
2408
- logger.info('SDK initialized successfully');
2414
+ this.authToken = authToken;
2409
2415
  }
2410
2416
  /**
2411
- * Create visitor ID based on storage mode
2417
+ * Set authentication token
2412
2418
  */
2413
- createVisitorId() {
2414
- // Anonymous mode: use temporary ID until consent
2415
- if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
2416
- const key = STORAGE_KEYS.VISITOR_ID + '_anon';
2417
- let anonId = getSessionStorage(key);
2418
- if (!anonId) {
2419
- anonId = 'anon_' + generateUUID();
2420
- setSessionStorage(key, anonId);
2421
- }
2422
- return anonId;
2419
+ setAuthToken(token) {
2420
+ this.authToken = token;
2421
+ }
2422
+ /**
2423
+ * Make authenticated API request
2424
+ */
2425
+ async request(endpoint, options = {}) {
2426
+ const url = `${this.apiEndpoint}${endpoint}`;
2427
+ const headers = {
2428
+ 'Content-Type': 'application/json',
2429
+ ...(options.headers || {}),
2430
+ };
2431
+ if (this.authToken) {
2432
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2423
2433
  }
2424
- // Cookie-less mode: use sessionStorage only
2425
- if (this.config.cookielessMode) {
2426
- let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
2427
- if (!visitorId) {
2428
- visitorId = generateUUID();
2429
- setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
2434
+ try {
2435
+ const response = await fetch(url, {
2436
+ ...options,
2437
+ headers,
2438
+ });
2439
+ const data = await response.json();
2440
+ if (!response.ok) {
2441
+ return {
2442
+ success: false,
2443
+ error: data.message || 'Request failed',
2444
+ status: response.status,
2445
+ };
2430
2446
  }
2431
- return visitorId;
2447
+ return {
2448
+ success: true,
2449
+ data: data.data || data,
2450
+ status: response.status,
2451
+ };
2452
+ }
2453
+ catch (error) {
2454
+ return {
2455
+ success: false,
2456
+ error: error instanceof Error ? error.message : 'Network error',
2457
+ status: 0,
2458
+ };
2432
2459
  }
2433
- // Normal mode
2434
- return getOrCreateVisitorId(this.config.useCookies);
2435
2460
  }
2461
+ // ============================================
2462
+ // TRIGGER MANAGEMENT
2463
+ // ============================================
2436
2464
  /**
2437
- * Create session ID
2465
+ * Get all event triggers
2438
2466
  */
2439
- createSessionId() {
2440
- return getOrCreateSessionId(this.config.sessionTimeout);
2467
+ async getTriggers() {
2468
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
2441
2469
  }
2442
2470
  /**
2443
- * Handle consent state changes
2471
+ * Get a single trigger by ID
2444
2472
  */
2445
- onConsentChange(state, previous) {
2446
- logger.debug('Consent changed:', { from: previous, to: state });
2447
- // If analytics consent was just granted
2448
- if (state.analytics && !previous.analytics) {
2449
- // Upgrade from anonymous ID to persistent ID
2450
- if (this.config.consent.anonymousMode) {
2451
- this.visitorId = getOrCreateVisitorId(this.config.useCookies);
2452
- logger.info('Upgraded from anonymous to persistent visitor ID');
2453
- }
2454
- // Flush buffered events
2455
- const buffered = this.consentManager.flushBuffer();
2456
- for (const event of buffered) {
2457
- // Update event with new visitor ID
2458
- event.visitorId = this.visitorId;
2459
- this.queue.push(event);
2460
- }
2461
- }
2473
+ async getTrigger(triggerId) {
2474
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
2462
2475
  }
2463
2476
  /**
2464
- * Initialize enabled plugins
2465
- * Handles both sync and async plugin init methods
2477
+ * Create a new event trigger
2466
2478
  */
2467
- initPlugins() {
2468
- const pluginsToLoad = this.config.plugins;
2469
- // Skip pageView plugin if autoPageView is disabled
2470
- const filteredPlugins = this.config.autoPageView
2471
- ? pluginsToLoad
2472
- : pluginsToLoad.filter((p) => p !== 'pageView');
2473
- for (const pluginName of filteredPlugins) {
2474
- try {
2475
- const plugin = getPlugin(pluginName);
2476
- // Handle both sync and async init (fire-and-forget for async)
2477
- const result = plugin.init(this);
2478
- if (result instanceof Promise) {
2479
- result.catch((error) => {
2480
- logger.error(`Async plugin init failed: ${pluginName}`, error);
2481
- });
2482
- }
2483
- this.plugins.push(plugin);
2484
- logger.debug(`Plugin loaded: ${pluginName}`);
2485
- }
2486
- catch (error) {
2487
- logger.error(`Failed to load plugin: ${pluginName}`, error);
2488
- }
2479
+ async createTrigger(trigger) {
2480
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
2481
+ method: 'POST',
2482
+ body: JSON.stringify(trigger),
2483
+ });
2484
+ // Cache the trigger locally if successful
2485
+ if (result.success && result.data?._id) {
2486
+ this.triggers.set(result.data._id, result.data);
2489
2487
  }
2488
+ return result;
2490
2489
  }
2491
2490
  /**
2492
- * Track a custom event
2491
+ * Update an existing trigger
2493
2492
  */
2494
- track(eventType, eventName, properties = {}) {
2495
- if (!this.isInitialized) {
2496
- logger.warn('SDK not initialized, event dropped');
2497
- return;
2498
- }
2499
- const event = {
2500
- workspaceId: this.workspaceId,
2501
- visitorId: this.visitorId,
2502
- sessionId: this.sessionId,
2503
- eventType: eventType,
2504
- eventName,
2505
- url: typeof window !== 'undefined' ? window.location.href : '',
2506
- referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
2507
- properties,
2508
- device: getDeviceInfo(),
2509
- ...getUTMParams(),
2510
- timestamp: new Date().toISOString(),
2511
- sdkVersion: SDK_VERSION,
2512
- };
2513
- // Check consent before tracking
2514
- if (!this.consentManager.canTrack()) {
2515
- // Buffer event for later if waitForConsent is enabled
2516
- if (this.config.consent.waitForConsent) {
2517
- this.consentManager.bufferEvent(event);
2518
- return;
2519
- }
2520
- // Otherwise drop the event
2521
- logger.debug('Event dropped (no consent):', eventName);
2522
- return;
2493
+ async updateTrigger(triggerId, updates) {
2494
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2495
+ method: 'PUT',
2496
+ body: JSON.stringify(updates),
2497
+ });
2498
+ // Update cache if successful
2499
+ if (result.success && result.data?._id) {
2500
+ this.triggers.set(result.data._id, result.data);
2523
2501
  }
2524
- this.queue.push(event);
2525
- logger.debug('Event tracked:', eventName, properties);
2502
+ return result;
2526
2503
  }
2527
2504
  /**
2528
- * Track a page view
2505
+ * Delete a trigger
2529
2506
  */
2530
- page(name, properties = {}) {
2531
- const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
2532
- this.track('page_view', pageName, {
2533
- ...properties,
2534
- path: typeof window !== 'undefined' ? window.location.pathname : '',
2507
+ async deleteTrigger(triggerId) {
2508
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2509
+ method: 'DELETE',
2535
2510
  });
2511
+ // Remove from cache if successful
2512
+ if (result.success) {
2513
+ this.triggers.delete(triggerId);
2514
+ }
2515
+ return result;
2536
2516
  }
2537
2517
  /**
2538
- * Identify a visitor
2539
- */
2540
- async identify(email, traits = {}) {
2541
- if (!email) {
2542
- logger.warn('Email is required for identification');
2543
- return;
2544
- }
2545
- logger.info('Identifying visitor:', email);
2546
- const result = await this.transport.sendIdentify({
2547
- workspaceId: this.workspaceId,
2548
- visitorId: this.visitorId,
2549
- email,
2550
- properties: traits,
2551
- });
2552
- if (result.success) {
2553
- logger.info('Visitor identified successfully');
2554
- this.pendingIdentify = null;
2555
- }
2556
- else {
2557
- logger.error('Failed to identify visitor:', result.error);
2558
- // Store for retry on next flush
2559
- this.pendingIdentify = { email, traits };
2560
- }
2561
- }
2562
- /**
2563
- * Retry pending identify call
2564
- */
2565
- async retryPendingIdentify() {
2566
- if (!this.pendingIdentify)
2567
- return;
2568
- const { email, traits } = this.pendingIdentify;
2569
- this.pendingIdentify = null;
2570
- await this.identify(email, traits);
2571
- }
2572
- /**
2573
- * Update consent state
2574
- */
2575
- consent(state) {
2576
- this.consentManager.update(state);
2577
- }
2578
- /**
2579
- * Get current consent state
2580
- */
2581
- getConsentState() {
2582
- return this.consentManager.getState();
2583
- }
2584
- /**
2585
- * Toggle debug mode
2586
- */
2587
- debug(enabled) {
2588
- logger.enabled = enabled;
2589
- logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
2590
- }
2591
- /**
2592
- * Get visitor ID
2593
- */
2594
- getVisitorId() {
2595
- return this.visitorId;
2596
- }
2597
- /**
2598
- * Get session ID
2599
- */
2600
- getSessionId() {
2601
- return this.sessionId;
2602
- }
2603
- /**
2604
- * Get workspace ID
2605
- */
2606
- getWorkspaceId() {
2607
- return this.workspaceId;
2608
- }
2609
- /**
2610
- * Get current configuration
2611
- */
2612
- getConfig() {
2613
- return { ...this.config };
2614
- }
2615
- /**
2616
- * Force flush event queue
2617
- */
2618
- async flush() {
2619
- await this.retryPendingIdentify();
2620
- await this.queue.flush();
2621
- }
2622
- /**
2623
- * Reset visitor and session (for logout)
2624
- */
2625
- reset() {
2626
- logger.info('Resetting visitor data');
2627
- resetIds(this.config.useCookies);
2628
- this.visitorId = this.createVisitorId();
2629
- this.sessionId = this.createSessionId();
2630
- this.queue.clear();
2631
- }
2632
- /**
2633
- * Delete all stored user data (GDPR right-to-erasure)
2634
- */
2635
- deleteData() {
2636
- logger.info('Deleting all user data (GDPR request)');
2637
- // Clear queue
2638
- this.queue.clear();
2639
- // Reset consent
2640
- this.consentManager.reset();
2641
- // Clear all stored IDs
2642
- resetIds(this.config.useCookies);
2643
- // Clear session storage items
2644
- if (typeof sessionStorage !== 'undefined') {
2645
- try {
2646
- sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2647
- sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
2648
- sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
2649
- sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
2650
- }
2651
- catch {
2652
- // Ignore errors
2653
- }
2654
- }
2655
- // Clear localStorage items
2656
- if (typeof localStorage !== 'undefined') {
2657
- try {
2658
- localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2659
- localStorage.removeItem(STORAGE_KEYS.CONSENT);
2660
- localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
2661
- }
2662
- catch {
2663
- // Ignore errors
2664
- }
2665
- }
2666
- // Generate new IDs
2667
- this.visitorId = this.createVisitorId();
2668
- this.sessionId = this.createSessionId();
2669
- logger.info('All user data deleted');
2670
- }
2671
- /**
2672
- * Destroy tracker and cleanup
2673
- */
2674
- async destroy() {
2675
- logger.info('Destroying tracker');
2676
- // Flush any remaining events (await to ensure completion)
2677
- await this.queue.flush();
2678
- // Destroy plugins
2679
- for (const plugin of this.plugins) {
2680
- if (plugin.destroy) {
2681
- plugin.destroy();
2682
- }
2683
- }
2684
- this.plugins = [];
2685
- // Destroy queue
2686
- this.queue.destroy();
2687
- this.isInitialized = false;
2688
- }
2689
- }
2690
-
2691
- /**
2692
- * Clianta SDK - Event Triggers Manager
2693
- * Manages event-driven automation and email notifications
2694
- */
2695
- /**
2696
- * Event Triggers Manager
2697
- * Handles event-driven automation based on CRM actions
2698
- *
2699
- * Similar to:
2700
- * - Salesforce: Process Builder, Flow Automation
2701
- * - HubSpot: Workflows, Email Sequences
2702
- * - Pipedrive: Workflow Automation
2703
- */
2704
- class EventTriggersManager {
2705
- constructor(apiEndpoint, workspaceId, authToken) {
2706
- this.triggers = new Map();
2707
- this.listeners = new Map();
2708
- this.apiEndpoint = apiEndpoint;
2709
- this.workspaceId = workspaceId;
2710
- this.authToken = authToken;
2711
- }
2712
- /**
2713
- * Set authentication token
2714
- */
2715
- setAuthToken(token) {
2716
- this.authToken = token;
2717
- }
2718
- /**
2719
- * Make authenticated API request
2720
- */
2721
- async request(endpoint, options = {}) {
2722
- const url = `${this.apiEndpoint}${endpoint}`;
2723
- const headers = {
2724
- 'Content-Type': 'application/json',
2725
- ...(options.headers || {}),
2726
- };
2727
- if (this.authToken) {
2728
- headers['Authorization'] = `Bearer ${this.authToken}`;
2729
- }
2730
- try {
2731
- const response = await fetch(url, {
2732
- ...options,
2733
- headers,
2734
- });
2735
- const data = await response.json();
2736
- if (!response.ok) {
2737
- return {
2738
- success: false,
2739
- error: data.message || 'Request failed',
2740
- status: response.status,
2741
- };
2742
- }
2743
- return {
2744
- success: true,
2745
- data: data.data || data,
2746
- status: response.status,
2747
- };
2748
- }
2749
- catch (error) {
2750
- return {
2751
- success: false,
2752
- error: error instanceof Error ? error.message : 'Network error',
2753
- status: 0,
2754
- };
2755
- }
2756
- }
2757
- // ============================================
2758
- // TRIGGER MANAGEMENT
2759
- // ============================================
2760
- /**
2761
- * Get all event triggers
2762
- */
2763
- async getTriggers() {
2764
- return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
2765
- }
2766
- /**
2767
- * Get a single trigger by ID
2768
- */
2769
- async getTrigger(triggerId) {
2770
- return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
2771
- }
2772
- /**
2773
- * Create a new event trigger
2774
- */
2775
- async createTrigger(trigger) {
2776
- const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
2777
- method: 'POST',
2778
- body: JSON.stringify(trigger),
2779
- });
2780
- // Cache the trigger locally if successful
2781
- if (result.success && result.data?._id) {
2782
- this.triggers.set(result.data._id, result.data);
2783
- }
2784
- return result;
2785
- }
2786
- /**
2787
- * Update an existing trigger
2788
- */
2789
- async updateTrigger(triggerId, updates) {
2790
- const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2791
- method: 'PUT',
2792
- body: JSON.stringify(updates),
2793
- });
2794
- // Update cache if successful
2795
- if (result.success && result.data?._id) {
2796
- this.triggers.set(result.data._id, result.data);
2797
- }
2798
- return result;
2799
- }
2800
- /**
2801
- * Delete a trigger
2802
- */
2803
- async deleteTrigger(triggerId) {
2804
- const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2805
- method: 'DELETE',
2806
- });
2807
- // Remove from cache if successful
2808
- if (result.success) {
2809
- this.triggers.delete(triggerId);
2810
- }
2811
- return result;
2812
- }
2813
- /**
2814
- * Activate a trigger
2518
+ * Activate a trigger
2815
2519
  */
2816
2520
  async activateTrigger(triggerId) {
2817
2521
  return this.updateTrigger(triggerId, { isActive: true });
@@ -3130,19 +2834,29 @@ class EventTriggersManager {
3130
2834
  * CRM API Client for managing contacts and opportunities
3131
2835
  */
3132
2836
  class CRMClient {
3133
- constructor(apiEndpoint, workspaceId, authToken) {
2837
+ constructor(apiEndpoint, workspaceId, authToken, apiKey) {
3134
2838
  this.apiEndpoint = apiEndpoint;
3135
2839
  this.workspaceId = workspaceId;
3136
2840
  this.authToken = authToken;
2841
+ this.apiKey = apiKey;
3137
2842
  this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
3138
2843
  }
3139
2844
  /**
3140
- * Set authentication token for API requests
2845
+ * Set authentication token for API requests (user JWT)
3141
2846
  */
3142
2847
  setAuthToken(token) {
3143
2848
  this.authToken = token;
2849
+ this.apiKey = undefined;
3144
2850
  this.triggers.setAuthToken(token);
3145
2851
  }
2852
+ /**
2853
+ * Set workspace API key for server-to-server requests.
2854
+ * Use this instead of setAuthToken when integrating from an external app.
2855
+ */
2856
+ setApiKey(key) {
2857
+ this.apiKey = key;
2858
+ this.authToken = undefined;
2859
+ }
3146
2860
  /**
3147
2861
  * Validate required parameter exists
3148
2862
  * @throws {Error} if value is null/undefined or empty string
@@ -3161,7 +2875,10 @@ class CRMClient {
3161
2875
  'Content-Type': 'application/json',
3162
2876
  ...(options.headers || {}),
3163
2877
  };
3164
- if (this.authToken) {
2878
+ if (this.apiKey) {
2879
+ headers['X-Api-Key'] = this.apiKey;
2880
+ }
2881
+ else if (this.authToken) {
3165
2882
  headers['Authorization'] = `Bearer ${this.authToken}`;
3166
2883
  }
3167
2884
  try {
@@ -3192,6 +2909,65 @@ class CRMClient {
3192
2909
  }
3193
2910
  }
3194
2911
  // ============================================
2912
+ // INBOUND EVENTS API (API-key authenticated)
2913
+ // ============================================
2914
+ /**
2915
+ * Send an inbound event from an external app (e.g. user signup on client website).
2916
+ * Requires the client to be initialized with an API key via setApiKey() or the constructor.
2917
+ *
2918
+ * The contact is upserted in the CRM and matching workflow automations fire automatically.
2919
+ *
2920
+ * @example
2921
+ * const crm = new CRMClient('https://api.clianta.online', 'WORKSPACE_ID');
2922
+ * crm.setApiKey('mm_live_...');
2923
+ *
2924
+ * await crm.sendEvent({
2925
+ * event: 'user.registered',
2926
+ * contact: { email: 'alice@example.com', firstName: 'Alice' },
2927
+ * data: { plan: 'free', signupSource: 'homepage' },
2928
+ * });
2929
+ */
2930
+ async sendEvent(payload) {
2931
+ const url = `${this.apiEndpoint}/api/public/events`;
2932
+ const headers = { 'Content-Type': 'application/json' };
2933
+ if (this.apiKey) {
2934
+ headers['X-Api-Key'] = this.apiKey;
2935
+ }
2936
+ else if (this.authToken) {
2937
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2938
+ }
2939
+ try {
2940
+ const response = await fetch(url, {
2941
+ method: 'POST',
2942
+ headers,
2943
+ body: JSON.stringify(payload),
2944
+ });
2945
+ const data = await response.json();
2946
+ if (!response.ok) {
2947
+ return {
2948
+ success: false,
2949
+ contactCreated: false,
2950
+ event: payload.event,
2951
+ error: data.error || 'Request failed',
2952
+ };
2953
+ }
2954
+ return {
2955
+ success: data.success,
2956
+ contactCreated: data.contactCreated,
2957
+ contactId: data.contactId,
2958
+ event: data.event,
2959
+ };
2960
+ }
2961
+ catch (error) {
2962
+ return {
2963
+ success: false,
2964
+ contactCreated: false,
2965
+ event: payload.event,
2966
+ error: error instanceof Error ? error.message : 'Network error',
2967
+ };
2968
+ }
2969
+ }
2970
+ // ============================================
3195
2971
  // CONTACTS API
3196
2972
  // ============================================
3197
2973
  /**
@@ -3375,319 +3151,671 @@ class CRMClient {
3375
3151
  return this.request(endpoint);
3376
3152
  }
3377
3153
  /**
3378
- * Get deals/opportunities belonging to a company
3154
+ * Get deals/opportunities belonging to a company
3155
+ */
3156
+ async getCompanyDeals(companyId, params) {
3157
+ const queryParams = new URLSearchParams();
3158
+ if (params?.page)
3159
+ queryParams.set('page', params.page.toString());
3160
+ if (params?.limit)
3161
+ queryParams.set('limit', params.limit.toString());
3162
+ const query = queryParams.toString();
3163
+ const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
3164
+ return this.request(endpoint);
3165
+ }
3166
+ // ============================================
3167
+ // PIPELINES API
3168
+ // ============================================
3169
+ /**
3170
+ * Get all pipelines
3171
+ */
3172
+ async getPipelines() {
3173
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
3174
+ }
3175
+ /**
3176
+ * Get a single pipeline by ID
3177
+ */
3178
+ async getPipeline(pipelineId) {
3179
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
3180
+ }
3181
+ /**
3182
+ * Create a new pipeline
3183
+ */
3184
+ async createPipeline(pipeline) {
3185
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
3186
+ method: 'POST',
3187
+ body: JSON.stringify(pipeline),
3188
+ });
3189
+ }
3190
+ /**
3191
+ * Update an existing pipeline
3192
+ */
3193
+ async updatePipeline(pipelineId, updates) {
3194
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3195
+ method: 'PUT',
3196
+ body: JSON.stringify(updates),
3197
+ });
3198
+ }
3199
+ /**
3200
+ * Delete a pipeline
3201
+ */
3202
+ async deletePipeline(pipelineId) {
3203
+ return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3204
+ method: 'DELETE',
3205
+ });
3206
+ }
3207
+ // ============================================
3208
+ // TASKS API
3209
+ // ============================================
3210
+ /**
3211
+ * Get all tasks with pagination
3212
+ */
3213
+ async getTasks(params) {
3214
+ const queryParams = new URLSearchParams();
3215
+ if (params?.page)
3216
+ queryParams.set('page', params.page.toString());
3217
+ if (params?.limit)
3218
+ queryParams.set('limit', params.limit.toString());
3219
+ if (params?.status)
3220
+ queryParams.set('status', params.status);
3221
+ if (params?.priority)
3222
+ queryParams.set('priority', params.priority);
3223
+ if (params?.contactId)
3224
+ queryParams.set('contactId', params.contactId);
3225
+ if (params?.companyId)
3226
+ queryParams.set('companyId', params.companyId);
3227
+ if (params?.opportunityId)
3228
+ queryParams.set('opportunityId', params.opportunityId);
3229
+ const query = queryParams.toString();
3230
+ const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
3231
+ return this.request(endpoint);
3232
+ }
3233
+ /**
3234
+ * Get a single task by ID
3235
+ */
3236
+ async getTask(taskId) {
3237
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
3238
+ }
3239
+ /**
3240
+ * Create a new task
3241
+ */
3242
+ async createTask(task) {
3243
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
3244
+ method: 'POST',
3245
+ body: JSON.stringify(task),
3246
+ });
3247
+ }
3248
+ /**
3249
+ * Update an existing task
3250
+ */
3251
+ async updateTask(taskId, updates) {
3252
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3253
+ method: 'PUT',
3254
+ body: JSON.stringify(updates),
3255
+ });
3256
+ }
3257
+ /**
3258
+ * Mark a task as completed
3259
+ */
3260
+ async completeTask(taskId) {
3261
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
3262
+ method: 'PATCH',
3263
+ });
3264
+ }
3265
+ /**
3266
+ * Delete a task
3267
+ */
3268
+ async deleteTask(taskId) {
3269
+ return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3270
+ method: 'DELETE',
3271
+ });
3272
+ }
3273
+ // ============================================
3274
+ // ACTIVITIES API
3275
+ // ============================================
3276
+ /**
3277
+ * Get activities for a contact
3278
+ */
3279
+ async getContactActivities(contactId, params) {
3280
+ const queryParams = new URLSearchParams();
3281
+ if (params?.page)
3282
+ queryParams.set('page', params.page.toString());
3283
+ if (params?.limit)
3284
+ queryParams.set('limit', params.limit.toString());
3285
+ if (params?.type)
3286
+ queryParams.set('type', params.type);
3287
+ const query = queryParams.toString();
3288
+ const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3289
+ return this.request(endpoint);
3290
+ }
3291
+ /**
3292
+ * Get activities for an opportunity/deal
3293
+ */
3294
+ async getOpportunityActivities(opportunityId, params) {
3295
+ const queryParams = new URLSearchParams();
3296
+ if (params?.page)
3297
+ queryParams.set('page', params.page.toString());
3298
+ if (params?.limit)
3299
+ queryParams.set('limit', params.limit.toString());
3300
+ if (params?.type)
3301
+ queryParams.set('type', params.type);
3302
+ const query = queryParams.toString();
3303
+ const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
3304
+ return this.request(endpoint);
3305
+ }
3306
+ /**
3307
+ * Create a new activity
3308
+ */
3309
+ async createActivity(activity) {
3310
+ // Determine the correct endpoint based on related entity
3311
+ let endpoint;
3312
+ if (activity.opportunityId) {
3313
+ endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
3314
+ }
3315
+ else if (activity.contactId) {
3316
+ endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
3317
+ }
3318
+ else {
3319
+ endpoint = `/api/workspaces/${this.workspaceId}/activities`;
3320
+ }
3321
+ return this.request(endpoint, {
3322
+ method: 'POST',
3323
+ body: JSON.stringify(activity),
3324
+ });
3325
+ }
3326
+ /**
3327
+ * Update an existing activity
3328
+ */
3329
+ async updateActivity(activityId, updates) {
3330
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3331
+ method: 'PATCH',
3332
+ body: JSON.stringify(updates),
3333
+ });
3334
+ }
3335
+ /**
3336
+ * Delete an activity
3337
+ */
3338
+ async deleteActivity(activityId) {
3339
+ return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3340
+ method: 'DELETE',
3341
+ });
3342
+ }
3343
+ /**
3344
+ * Log a call activity
3345
+ */
3346
+ async logCall(data) {
3347
+ return this.createActivity({
3348
+ type: 'call',
3349
+ title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
3350
+ direction: data.direction,
3351
+ duration: data.duration,
3352
+ outcome: data.outcome,
3353
+ description: data.notes,
3354
+ contactId: data.contactId,
3355
+ opportunityId: data.opportunityId,
3356
+ });
3357
+ }
3358
+ /**
3359
+ * Log a meeting activity
3360
+ */
3361
+ async logMeeting(data) {
3362
+ return this.createActivity({
3363
+ type: 'meeting',
3364
+ title: data.title,
3365
+ duration: data.duration,
3366
+ outcome: data.outcome,
3367
+ description: data.notes,
3368
+ contactId: data.contactId,
3369
+ opportunityId: data.opportunityId,
3370
+ });
3371
+ }
3372
+ /**
3373
+ * Add a note to a contact or opportunity
3374
+ */
3375
+ async addNote(data) {
3376
+ return this.createActivity({
3377
+ type: 'note',
3378
+ title: 'Note',
3379
+ description: data.content,
3380
+ contactId: data.contactId,
3381
+ opportunityId: data.opportunityId,
3382
+ });
3383
+ }
3384
+ // ============================================
3385
+ // EMAIL TEMPLATES API
3386
+ // ============================================
3387
+ /**
3388
+ * Get all email templates
3379
3389
  */
3380
- async getCompanyDeals(companyId, params) {
3390
+ async getEmailTemplates(params) {
3381
3391
  const queryParams = new URLSearchParams();
3382
3392
  if (params?.page)
3383
3393
  queryParams.set('page', params.page.toString());
3384
3394
  if (params?.limit)
3385
3395
  queryParams.set('limit', params.limit.toString());
3386
3396
  const query = queryParams.toString();
3387
- const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
3397
+ const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
3388
3398
  return this.request(endpoint);
3389
3399
  }
3390
- // ============================================
3391
- // PIPELINES API
3392
- // ============================================
3393
- /**
3394
- * Get all pipelines
3395
- */
3396
- async getPipelines() {
3397
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
3398
- }
3399
3400
  /**
3400
- * Get a single pipeline by ID
3401
+ * Get a single email template by ID
3401
3402
  */
3402
- async getPipeline(pipelineId) {
3403
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
3403
+ async getEmailTemplate(templateId) {
3404
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
3404
3405
  }
3405
3406
  /**
3406
- * Create a new pipeline
3407
+ * Create a new email template
3407
3408
  */
3408
- async createPipeline(pipeline) {
3409
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
3409
+ async createEmailTemplate(template) {
3410
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
3410
3411
  method: 'POST',
3411
- body: JSON.stringify(pipeline),
3412
+ body: JSON.stringify(template),
3412
3413
  });
3413
3414
  }
3414
3415
  /**
3415
- * Update an existing pipeline
3416
+ * Update an email template
3416
3417
  */
3417
- async updatePipeline(pipelineId, updates) {
3418
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3418
+ async updateEmailTemplate(templateId, updates) {
3419
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3419
3420
  method: 'PUT',
3420
3421
  body: JSON.stringify(updates),
3421
3422
  });
3422
3423
  }
3423
3424
  /**
3424
- * Delete a pipeline
3425
+ * Delete an email template
3425
3426
  */
3426
- async deletePipeline(pipelineId) {
3427
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3427
+ async deleteEmailTemplate(templateId) {
3428
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3428
3429
  method: 'DELETE',
3429
3430
  });
3430
3431
  }
3432
+ /**
3433
+ * Send an email using a template
3434
+ */
3435
+ async sendEmail(data) {
3436
+ return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3437
+ method: 'POST',
3438
+ body: JSON.stringify(data),
3439
+ });
3440
+ }
3431
3441
  // ============================================
3432
- // TASKS API
3442
+ // EVENT TRIGGERS API (delegated to triggers manager)
3433
3443
  // ============================================
3434
3444
  /**
3435
- * Get all tasks with pagination
3445
+ * Get all event triggers
3436
3446
  */
3437
- async getTasks(params) {
3438
- const queryParams = new URLSearchParams();
3439
- if (params?.page)
3440
- queryParams.set('page', params.page.toString());
3441
- if (params?.limit)
3442
- queryParams.set('limit', params.limit.toString());
3443
- if (params?.status)
3444
- queryParams.set('status', params.status);
3445
- if (params?.priority)
3446
- queryParams.set('priority', params.priority);
3447
- if (params?.contactId)
3448
- queryParams.set('contactId', params.contactId);
3449
- if (params?.companyId)
3450
- queryParams.set('companyId', params.companyId);
3451
- if (params?.opportunityId)
3452
- queryParams.set('opportunityId', params.opportunityId);
3453
- const query = queryParams.toString();
3454
- const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
3455
- return this.request(endpoint);
3447
+ async getEventTriggers() {
3448
+ return this.triggers.getTriggers();
3456
3449
  }
3457
3450
  /**
3458
- * Get a single task by ID
3451
+ * Create a new event trigger
3459
3452
  */
3460
- async getTask(taskId) {
3461
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
3453
+ async createEventTrigger(trigger) {
3454
+ return this.triggers.createTrigger(trigger);
3462
3455
  }
3463
3456
  /**
3464
- * Create a new task
3457
+ * Update an event trigger
3465
3458
  */
3466
- async createTask(task) {
3467
- return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
3468
- method: 'POST',
3469
- body: JSON.stringify(task),
3470
- });
3459
+ async updateEventTrigger(triggerId, updates) {
3460
+ return this.triggers.updateTrigger(triggerId, updates);
3471
3461
  }
3472
3462
  /**
3473
- * Update an existing task
3463
+ * Delete an event trigger
3474
3464
  */
3475
- async updateTask(taskId, updates) {
3476
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3477
- method: 'PUT',
3478
- body: JSON.stringify(updates),
3465
+ async deleteEventTrigger(triggerId) {
3466
+ return this.triggers.deleteTrigger(triggerId);
3467
+ }
3468
+ }
3469
+
3470
+ /**
3471
+ * Clianta SDK - Main Tracker Class
3472
+ * @see SDK_VERSION in core/config.ts
3473
+ */
3474
+ /**
3475
+ * Main Clianta Tracker Class
3476
+ */
3477
+ class Tracker {
3478
+ constructor(workspaceId, userConfig = {}) {
3479
+ this.plugins = [];
3480
+ this.isInitialized = false;
3481
+ /** contactId after a successful identify() call */
3482
+ this.contactId = null;
3483
+ /** Pending identify retry on next flush */
3484
+ this.pendingIdentify = null;
3485
+ if (!workspaceId) {
3486
+ throw new Error('[Clianta] Workspace ID is required');
3487
+ }
3488
+ this.workspaceId = workspaceId;
3489
+ this.config = mergeConfig(userConfig);
3490
+ // Setup debug mode
3491
+ logger.enabled = this.config.debug;
3492
+ logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
3493
+ // Initialize consent manager
3494
+ this.consentManager = new ConsentManager({
3495
+ ...this.config.consent,
3496
+ onConsentChange: (state, previous) => {
3497
+ this.onConsentChange(state, previous);
3498
+ },
3499
+ });
3500
+ // Initialize transport and queue
3501
+ this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
3502
+ this.queue = new EventQueue(this.transport, {
3503
+ batchSize: this.config.batchSize,
3504
+ flushInterval: this.config.flushInterval,
3479
3505
  });
3506
+ // Get or create visitor and session IDs based on mode
3507
+ this.visitorId = this.createVisitorId();
3508
+ this.sessionId = this.createSessionId();
3509
+ logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
3510
+ // Initialize plugins
3511
+ this.initPlugins();
3512
+ this.isInitialized = true;
3513
+ logger.info('SDK initialized successfully');
3480
3514
  }
3481
3515
  /**
3482
- * Mark a task as completed
3516
+ * Create visitor ID based on storage mode
3483
3517
  */
3484
- async completeTask(taskId) {
3485
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
3486
- method: 'PATCH',
3487
- });
3518
+ createVisitorId() {
3519
+ // Anonymous mode: use temporary ID until consent
3520
+ if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
3521
+ const key = STORAGE_KEYS.VISITOR_ID + '_anon';
3522
+ let anonId = getSessionStorage(key);
3523
+ if (!anonId) {
3524
+ anonId = 'anon_' + generateUUID();
3525
+ setSessionStorage(key, anonId);
3526
+ }
3527
+ return anonId;
3528
+ }
3529
+ // Cookie-less mode: use sessionStorage only
3530
+ if (this.config.cookielessMode) {
3531
+ let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
3532
+ if (!visitorId) {
3533
+ visitorId = generateUUID();
3534
+ setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
3535
+ }
3536
+ return visitorId;
3537
+ }
3538
+ // Normal mode
3539
+ return getOrCreateVisitorId(this.config.useCookies);
3488
3540
  }
3489
3541
  /**
3490
- * Delete a task
3542
+ * Create session ID
3491
3543
  */
3492
- async deleteTask(taskId) {
3493
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3494
- method: 'DELETE',
3495
- });
3544
+ createSessionId() {
3545
+ return getOrCreateSessionId(this.config.sessionTimeout);
3496
3546
  }
3497
- // ============================================
3498
- // ACTIVITIES API
3499
- // ============================================
3500
3547
  /**
3501
- * Get activities for a contact
3548
+ * Handle consent state changes
3502
3549
  */
3503
- async getContactActivities(contactId, params) {
3504
- const queryParams = new URLSearchParams();
3505
- if (params?.page)
3506
- queryParams.set('page', params.page.toString());
3507
- if (params?.limit)
3508
- queryParams.set('limit', params.limit.toString());
3509
- if (params?.type)
3510
- queryParams.set('type', params.type);
3511
- const query = queryParams.toString();
3512
- const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3513
- return this.request(endpoint);
3550
+ onConsentChange(state, previous) {
3551
+ logger.debug('Consent changed:', { from: previous, to: state });
3552
+ // If analytics consent was just granted
3553
+ if (state.analytics && !previous.analytics) {
3554
+ // Upgrade from anonymous ID to persistent ID
3555
+ if (this.config.consent.anonymousMode) {
3556
+ this.visitorId = getOrCreateVisitorId(this.config.useCookies);
3557
+ logger.info('Upgraded from anonymous to persistent visitor ID');
3558
+ }
3559
+ // Flush buffered events
3560
+ const buffered = this.consentManager.flushBuffer();
3561
+ for (const event of buffered) {
3562
+ // Update event with new visitor ID
3563
+ event.visitorId = this.visitorId;
3564
+ this.queue.push(event);
3565
+ }
3566
+ }
3514
3567
  }
3515
3568
  /**
3516
- * Get activities for an opportunity/deal
3569
+ * Initialize enabled plugins
3570
+ * Handles both sync and async plugin init methods
3517
3571
  */
3518
- async getOpportunityActivities(opportunityId, params) {
3519
- const queryParams = new URLSearchParams();
3520
- if (params?.page)
3521
- queryParams.set('page', params.page.toString());
3522
- if (params?.limit)
3523
- queryParams.set('limit', params.limit.toString());
3524
- if (params?.type)
3525
- queryParams.set('type', params.type);
3526
- const query = queryParams.toString();
3527
- const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
3528
- return this.request(endpoint);
3572
+ initPlugins() {
3573
+ const pluginsToLoad = this.config.plugins;
3574
+ // Skip pageView plugin if autoPageView is disabled
3575
+ const filteredPlugins = this.config.autoPageView
3576
+ ? pluginsToLoad
3577
+ : pluginsToLoad.filter((p) => p !== 'pageView');
3578
+ for (const pluginName of filteredPlugins) {
3579
+ try {
3580
+ const plugin = getPlugin(pluginName);
3581
+ // Handle both sync and async init (fire-and-forget for async)
3582
+ const result = plugin.init(this);
3583
+ if (result instanceof Promise) {
3584
+ result.catch((error) => {
3585
+ logger.error(`Async plugin init failed: ${pluginName}`, error);
3586
+ });
3587
+ }
3588
+ this.plugins.push(plugin);
3589
+ logger.debug(`Plugin loaded: ${pluginName}`);
3590
+ }
3591
+ catch (error) {
3592
+ logger.error(`Failed to load plugin: ${pluginName}`, error);
3593
+ }
3594
+ }
3529
3595
  }
3530
3596
  /**
3531
- * Create a new activity
3597
+ * Track a custom event
3532
3598
  */
3533
- async createActivity(activity) {
3534
- // Determine the correct endpoint based on related entity
3535
- let endpoint;
3536
- if (activity.opportunityId) {
3537
- endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
3599
+ track(eventType, eventName, properties = {}) {
3600
+ if (!this.isInitialized) {
3601
+ logger.warn('SDK not initialized, event dropped');
3602
+ return;
3538
3603
  }
3539
- else if (activity.contactId) {
3540
- endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
3604
+ const event = {
3605
+ workspaceId: this.workspaceId,
3606
+ visitorId: this.visitorId,
3607
+ sessionId: this.sessionId,
3608
+ eventType: eventType,
3609
+ eventName,
3610
+ url: typeof window !== 'undefined' ? window.location.href : '',
3611
+ referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
3612
+ properties: {
3613
+ ...properties,
3614
+ eventId: generateUUID(), // Unique ID for deduplication on retry
3615
+ },
3616
+ device: getDeviceInfo(),
3617
+ ...getUTMParams(),
3618
+ timestamp: new Date().toISOString(),
3619
+ sdkVersion: SDK_VERSION,
3620
+ };
3621
+ // Attach contactId if known (from a prior identify() call)
3622
+ if (this.contactId) {
3623
+ event.contactId = this.contactId;
3541
3624
  }
3542
- else {
3543
- endpoint = `/api/workspaces/${this.workspaceId}/activities`;
3625
+ // Check consent before tracking
3626
+ if (!this.consentManager.canTrack()) {
3627
+ // Buffer event for later if waitForConsent is enabled
3628
+ if (this.config.consent.waitForConsent) {
3629
+ this.consentManager.bufferEvent(event);
3630
+ return;
3631
+ }
3632
+ // Otherwise drop the event
3633
+ logger.debug('Event dropped (no consent):', eventName);
3634
+ return;
3544
3635
  }
3545
- return this.request(endpoint, {
3546
- method: 'POST',
3547
- body: JSON.stringify(activity),
3548
- });
3636
+ this.queue.push(event);
3637
+ logger.debug('Event tracked:', eventName, properties);
3549
3638
  }
3550
3639
  /**
3551
- * Update an existing activity
3640
+ * Track a page view
3552
3641
  */
3553
- async updateActivity(activityId, updates) {
3554
- return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3555
- method: 'PATCH',
3556
- body: JSON.stringify(updates),
3642
+ page(name, properties = {}) {
3643
+ const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
3644
+ this.track('page_view', pageName, {
3645
+ ...properties,
3646
+ path: typeof window !== 'undefined' ? window.location.pathname : '',
3557
3647
  });
3558
3648
  }
3559
3649
  /**
3560
- * Delete an activity
3650
+ * Identify a visitor.
3651
+ * Links the anonymous visitorId to a CRM contact and returns the contactId.
3652
+ * All subsequent track() calls will include the contactId automatically.
3561
3653
  */
3562
- async deleteActivity(activityId) {
3563
- return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3564
- method: 'DELETE',
3654
+ async identify(email, traits = {}) {
3655
+ if (!email) {
3656
+ logger.warn('Email is required for identification');
3657
+ return null;
3658
+ }
3659
+ logger.info('Identifying visitor:', email);
3660
+ const result = await this.transport.sendIdentify({
3661
+ workspaceId: this.workspaceId,
3662
+ visitorId: this.visitorId,
3663
+ email,
3664
+ properties: traits,
3565
3665
  });
3666
+ if (result.success) {
3667
+ logger.info('Visitor identified successfully, contactId:', result.contactId);
3668
+ // Store contactId so all future track() calls include it
3669
+ this.contactId = result.contactId ?? null;
3670
+ this.pendingIdentify = null;
3671
+ return this.contactId;
3672
+ }
3673
+ else {
3674
+ logger.error('Failed to identify visitor:', result.error);
3675
+ // Store for retry on next flush
3676
+ this.pendingIdentify = { email, traits };
3677
+ return null;
3678
+ }
3566
3679
  }
3567
3680
  /**
3568
- * Log a call activity
3681
+ * Send a server-side inbound event via the API key endpoint.
3682
+ * Convenience proxy to CRMClient.sendEvent() — requires apiKey in config.
3569
3683
  */
3570
- async logCall(data) {
3571
- return this.createActivity({
3572
- type: 'call',
3573
- title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
3574
- direction: data.direction,
3575
- duration: data.duration,
3576
- outcome: data.outcome,
3577
- description: data.notes,
3578
- contactId: data.contactId,
3579
- opportunityId: data.opportunityId,
3580
- });
3684
+ async sendEvent(payload) {
3685
+ const apiKey = this.config.apiKey;
3686
+ if (!apiKey) {
3687
+ logger.error('sendEvent() requires an apiKey in the SDK config');
3688
+ return { success: false, contactCreated: false, event: payload.event, error: 'No API key configured' };
3689
+ }
3690
+ const client = new CRMClient(this.config.apiEndpoint, this.workspaceId, undefined, apiKey);
3691
+ return client.sendEvent(payload);
3581
3692
  }
3582
3693
  /**
3583
- * Log a meeting activity
3694
+ * Retry pending identify call
3584
3695
  */
3585
- async logMeeting(data) {
3586
- return this.createActivity({
3587
- type: 'meeting',
3588
- title: data.title,
3589
- duration: data.duration,
3590
- outcome: data.outcome,
3591
- description: data.notes,
3592
- contactId: data.contactId,
3593
- opportunityId: data.opportunityId,
3594
- });
3696
+ async retryPendingIdentify() {
3697
+ if (!this.pendingIdentify)
3698
+ return;
3699
+ const { email, traits } = this.pendingIdentify;
3700
+ this.pendingIdentify = null;
3701
+ await this.identify(email, traits);
3595
3702
  }
3596
3703
  /**
3597
- * Add a note to a contact or opportunity
3704
+ * Update consent state
3598
3705
  */
3599
- async addNote(data) {
3600
- return this.createActivity({
3601
- type: 'note',
3602
- title: 'Note',
3603
- description: data.content,
3604
- contactId: data.contactId,
3605
- opportunityId: data.opportunityId,
3606
- });
3706
+ consent(state) {
3707
+ this.consentManager.update(state);
3607
3708
  }
3608
- // ============================================
3609
- // EMAIL TEMPLATES API
3610
- // ============================================
3611
3709
  /**
3612
- * Get all email templates
3710
+ * Get current consent state
3613
3711
  */
3614
- async getEmailTemplates(params) {
3615
- const queryParams = new URLSearchParams();
3616
- if (params?.page)
3617
- queryParams.set('page', params.page.toString());
3618
- if (params?.limit)
3619
- queryParams.set('limit', params.limit.toString());
3620
- const query = queryParams.toString();
3621
- const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
3622
- return this.request(endpoint);
3712
+ getConsentState() {
3713
+ return this.consentManager.getState();
3623
3714
  }
3624
3715
  /**
3625
- * Get a single email template by ID
3716
+ * Toggle debug mode
3626
3717
  */
3627
- async getEmailTemplate(templateId) {
3628
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
3718
+ debug(enabled) {
3719
+ logger.enabled = enabled;
3720
+ logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
3629
3721
  }
3630
3722
  /**
3631
- * Create a new email template
3723
+ * Get visitor ID
3632
3724
  */
3633
- async createEmailTemplate(template) {
3634
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
3635
- method: 'POST',
3636
- body: JSON.stringify(template),
3637
- });
3725
+ getVisitorId() {
3726
+ return this.visitorId;
3638
3727
  }
3639
3728
  /**
3640
- * Update an email template
3729
+ * Get session ID
3641
3730
  */
3642
- async updateEmailTemplate(templateId, updates) {
3643
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3644
- method: 'PUT',
3645
- body: JSON.stringify(updates),
3646
- });
3731
+ getSessionId() {
3732
+ return this.sessionId;
3647
3733
  }
3648
3734
  /**
3649
- * Delete an email template
3735
+ * Get workspace ID
3650
3736
  */
3651
- async deleteEmailTemplate(templateId) {
3652
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3653
- method: 'DELETE',
3654
- });
3737
+ getWorkspaceId() {
3738
+ return this.workspaceId;
3655
3739
  }
3656
3740
  /**
3657
- * Send an email using a template
3741
+ * Get current configuration
3658
3742
  */
3659
- async sendEmail(data) {
3660
- return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3661
- method: 'POST',
3662
- body: JSON.stringify(data),
3663
- });
3743
+ getConfig() {
3744
+ return { ...this.config };
3664
3745
  }
3665
- // ============================================
3666
- // EVENT TRIGGERS API (delegated to triggers manager)
3667
- // ============================================
3668
3746
  /**
3669
- * Get all event triggers
3747
+ * Force flush event queue
3670
3748
  */
3671
- async getEventTriggers() {
3672
- return this.triggers.getTriggers();
3749
+ async flush() {
3750
+ await this.retryPendingIdentify();
3751
+ await this.queue.flush();
3673
3752
  }
3674
3753
  /**
3675
- * Create a new event trigger
3754
+ * Reset visitor and session (for logout)
3676
3755
  */
3677
- async createEventTrigger(trigger) {
3678
- return this.triggers.createTrigger(trigger);
3756
+ reset() {
3757
+ logger.info('Resetting visitor data');
3758
+ resetIds(this.config.useCookies);
3759
+ this.visitorId = this.createVisitorId();
3760
+ this.sessionId = this.createSessionId();
3761
+ this.queue.clear();
3679
3762
  }
3680
3763
  /**
3681
- * Update an event trigger
3764
+ * Delete all stored user data (GDPR right-to-erasure)
3682
3765
  */
3683
- async updateEventTrigger(triggerId, updates) {
3684
- return this.triggers.updateTrigger(triggerId, updates);
3766
+ deleteData() {
3767
+ logger.info('Deleting all user data (GDPR request)');
3768
+ // Clear queue
3769
+ this.queue.clear();
3770
+ // Reset consent
3771
+ this.consentManager.reset();
3772
+ // Clear all stored IDs
3773
+ resetIds(this.config.useCookies);
3774
+ // Clear session storage items
3775
+ if (typeof sessionStorage !== 'undefined') {
3776
+ try {
3777
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
3778
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
3779
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
3780
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
3781
+ }
3782
+ catch {
3783
+ // Ignore errors
3784
+ }
3785
+ }
3786
+ // Clear localStorage items
3787
+ if (typeof localStorage !== 'undefined') {
3788
+ try {
3789
+ localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
3790
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
3791
+ localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
3792
+ }
3793
+ catch {
3794
+ // Ignore errors
3795
+ }
3796
+ }
3797
+ // Generate new IDs
3798
+ this.visitorId = this.createVisitorId();
3799
+ this.sessionId = this.createSessionId();
3800
+ logger.info('All user data deleted');
3685
3801
  }
3686
3802
  /**
3687
- * Delete an event trigger
3803
+ * Destroy tracker and cleanup
3688
3804
  */
3689
- async deleteEventTrigger(triggerId) {
3690
- return this.triggers.deleteTrigger(triggerId);
3805
+ async destroy() {
3806
+ logger.info('Destroying tracker');
3807
+ // Flush any remaining events (await to ensure completion)
3808
+ await this.queue.flush();
3809
+ // Destroy plugins
3810
+ for (const plugin of this.plugins) {
3811
+ if (plugin.destroy) {
3812
+ plugin.destroy();
3813
+ }
3814
+ }
3815
+ this.plugins = [];
3816
+ // Destroy queue
3817
+ this.queue.destroy();
3818
+ this.isInitialized = false;
3691
3819
  }
3692
3820
  }
3693
3821