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