@clianta/sdk 1.3.0 → 1.4.0

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