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