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