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