@clianta/sdk 1.5.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.5.1
2
+ * Clianta SDK v1.6.1
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -13,7 +13,7 @@ var react = require('react');
13
13
  * @see SDK_VERSION in core/config.ts
14
14
  */
15
15
  /** SDK Version */
16
- const SDK_VERSION = '1.4.0';
16
+ const SDK_VERSION = '1.6.1';
17
17
  /** Default API endpoint — reads from env or falls back to localhost */
18
18
  const getDefaultApiEndpoint = () => {
19
19
  // Build-time env var (works with Next.js, Vite, CRA, etc.)
@@ -45,8 +45,6 @@ const DEFAULT_PLUGINS = [
45
45
  const DEFAULT_CONFIG = {
46
46
  projectId: '',
47
47
  apiEndpoint: getDefaultApiEndpoint(),
48
- authToken: '',
49
- apiKey: '',
50
48
  debug: false,
51
49
  autoPageView: true,
52
50
  plugins: DEFAULT_PLUGINS,
@@ -2512,1189 +2510,6 @@ class ConsentManager {
2512
2510
  }
2513
2511
  }
2514
2512
 
2515
- /**
2516
- * Clianta SDK - Event Triggers Manager
2517
- * Manages event-driven automation and email notifications
2518
- */
2519
- /**
2520
- * Event Triggers Manager
2521
- * Handles event-driven automation based on CRM actions
2522
- *
2523
- * Similar to:
2524
- * - Salesforce: Process Builder, Flow Automation
2525
- * - HubSpot: Workflows, Email Sequences
2526
- * - Pipedrive: Workflow Automation
2527
- */
2528
- class EventTriggersManager {
2529
- constructor(apiEndpoint, workspaceId, authToken) {
2530
- this.triggers = new Map();
2531
- this.listeners = new Map();
2532
- this.apiEndpoint = apiEndpoint;
2533
- this.workspaceId = workspaceId;
2534
- this.authToken = authToken;
2535
- }
2536
- /**
2537
- * Set authentication token
2538
- */
2539
- setAuthToken(token) {
2540
- this.authToken = token;
2541
- }
2542
- /**
2543
- * Make authenticated API request
2544
- */
2545
- async request(endpoint, options = {}) {
2546
- const url = `${this.apiEndpoint}${endpoint}`;
2547
- const headers = {
2548
- 'Content-Type': 'application/json',
2549
- ...(options.headers || {}),
2550
- };
2551
- if (this.authToken) {
2552
- headers['Authorization'] = `Bearer ${this.authToken}`;
2553
- }
2554
- try {
2555
- const response = await fetch(url, {
2556
- ...options,
2557
- headers,
2558
- });
2559
- const data = await response.json();
2560
- if (!response.ok) {
2561
- return {
2562
- success: false,
2563
- error: data.message || 'Request failed',
2564
- status: response.status,
2565
- };
2566
- }
2567
- return {
2568
- success: true,
2569
- data: data.data || data,
2570
- status: response.status,
2571
- };
2572
- }
2573
- catch (error) {
2574
- return {
2575
- success: false,
2576
- error: error instanceof Error ? error.message : 'Network error',
2577
- status: 0,
2578
- };
2579
- }
2580
- }
2581
- // ============================================
2582
- // TRIGGER MANAGEMENT
2583
- // ============================================
2584
- /**
2585
- * Get all event triggers
2586
- */
2587
- async getTriggers() {
2588
- return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
2589
- }
2590
- /**
2591
- * Get a single trigger by ID
2592
- */
2593
- async getTrigger(triggerId) {
2594
- return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
2595
- }
2596
- /**
2597
- * Create a new event trigger
2598
- */
2599
- async createTrigger(trigger) {
2600
- const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
2601
- method: 'POST',
2602
- body: JSON.stringify(trigger),
2603
- });
2604
- // Cache the trigger locally if successful
2605
- if (result.success && result.data?._id) {
2606
- this.triggers.set(result.data._id, result.data);
2607
- }
2608
- return result;
2609
- }
2610
- /**
2611
- * Update an existing trigger
2612
- */
2613
- async updateTrigger(triggerId, updates) {
2614
- const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2615
- method: 'PUT',
2616
- body: JSON.stringify(updates),
2617
- });
2618
- // Update cache if successful
2619
- if (result.success && result.data?._id) {
2620
- this.triggers.set(result.data._id, result.data);
2621
- }
2622
- return result;
2623
- }
2624
- /**
2625
- * Delete a trigger
2626
- */
2627
- async deleteTrigger(triggerId) {
2628
- const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2629
- method: 'DELETE',
2630
- });
2631
- // Remove from cache if successful
2632
- if (result.success) {
2633
- this.triggers.delete(triggerId);
2634
- }
2635
- return result;
2636
- }
2637
- /**
2638
- * Activate a trigger
2639
- */
2640
- async activateTrigger(triggerId) {
2641
- return this.updateTrigger(triggerId, { isActive: true });
2642
- }
2643
- /**
2644
- * Deactivate a trigger
2645
- */
2646
- async deactivateTrigger(triggerId) {
2647
- return this.updateTrigger(triggerId, { isActive: false });
2648
- }
2649
- // ============================================
2650
- // EVENT HANDLING (CLIENT-SIDE)
2651
- // ============================================
2652
- /**
2653
- * Register a local event listener for client-side triggers
2654
- * This allows immediate client-side reactions to events
2655
- */
2656
- on(eventType, callback) {
2657
- if (!this.listeners.has(eventType)) {
2658
- this.listeners.set(eventType, new Set());
2659
- }
2660
- this.listeners.get(eventType).add(callback);
2661
- logger.debug(`Event listener registered: ${eventType}`);
2662
- }
2663
- /**
2664
- * Remove an event listener
2665
- */
2666
- off(eventType, callback) {
2667
- const listeners = this.listeners.get(eventType);
2668
- if (listeners) {
2669
- listeners.delete(callback);
2670
- }
2671
- }
2672
- /**
2673
- * Emit an event (client-side only)
2674
- * This will trigger any registered local listeners
2675
- */
2676
- emit(eventType, data) {
2677
- logger.debug(`Event emitted: ${eventType}`, data);
2678
- const listeners = this.listeners.get(eventType);
2679
- if (listeners) {
2680
- listeners.forEach(callback => {
2681
- try {
2682
- callback(data);
2683
- }
2684
- catch (error) {
2685
- logger.error(`Error in event listener for ${eventType}:`, error);
2686
- }
2687
- });
2688
- }
2689
- }
2690
- /**
2691
- * Check if conditions are met for a trigger
2692
- * Supports dynamic field evaluation including custom fields and nested paths
2693
- */
2694
- evaluateConditions(conditions, data) {
2695
- if (!conditions || conditions.length === 0) {
2696
- return true; // No conditions means always fire
2697
- }
2698
- return conditions.every(condition => {
2699
- // Support dot notation for nested fields (e.g., 'customFields.industry')
2700
- const fieldValue = condition.field.includes('.')
2701
- ? this.getNestedValue(data, condition.field)
2702
- : data[condition.field];
2703
- const targetValue = condition.value;
2704
- switch (condition.operator) {
2705
- case 'equals':
2706
- return fieldValue === targetValue;
2707
- case 'not_equals':
2708
- return fieldValue !== targetValue;
2709
- case 'contains':
2710
- return String(fieldValue).includes(String(targetValue));
2711
- case 'greater_than':
2712
- return Number(fieldValue) > Number(targetValue);
2713
- case 'less_than':
2714
- return Number(fieldValue) < Number(targetValue);
2715
- case 'in':
2716
- return Array.isArray(targetValue) && targetValue.includes(fieldValue);
2717
- case 'not_in':
2718
- return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
2719
- default:
2720
- return false;
2721
- }
2722
- });
2723
- }
2724
- /**
2725
- * Execute actions for a triggered event (client-side preview)
2726
- * Note: Actual execution happens on the backend
2727
- */
2728
- async executeActions(trigger, data) {
2729
- logger.info(`Executing actions for trigger: ${trigger.name}`);
2730
- for (const action of trigger.actions) {
2731
- try {
2732
- await this.executeAction(action, data);
2733
- }
2734
- catch (error) {
2735
- logger.error(`Failed to execute action:`, error);
2736
- }
2737
- }
2738
- }
2739
- /**
2740
- * Execute a single action
2741
- */
2742
- async executeAction(action, data) {
2743
- switch (action.type) {
2744
- case 'send_email':
2745
- await this.executeSendEmail(action, data);
2746
- break;
2747
- case 'webhook':
2748
- await this.executeWebhook(action, data);
2749
- break;
2750
- case 'create_task':
2751
- await this.executeCreateTask(action, data);
2752
- break;
2753
- case 'update_contact':
2754
- await this.executeUpdateContact(action, data);
2755
- break;
2756
- default:
2757
- logger.warn(`Unknown action type:`, action);
2758
- }
2759
- }
2760
- /**
2761
- * Execute send email action (via backend API)
2762
- */
2763
- async executeSendEmail(action, data) {
2764
- logger.debug('Sending email:', action);
2765
- const payload = {
2766
- to: this.replaceVariables(action.to, data),
2767
- subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
2768
- body: action.body ? this.replaceVariables(action.body, data) : undefined,
2769
- templateId: action.templateId,
2770
- cc: action.cc,
2771
- bcc: action.bcc,
2772
- from: action.from,
2773
- delayMinutes: action.delayMinutes,
2774
- };
2775
- await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
2776
- method: 'POST',
2777
- body: JSON.stringify(payload),
2778
- });
2779
- }
2780
- /**
2781
- * Execute webhook action
2782
- */
2783
- async executeWebhook(action, data) {
2784
- logger.debug('Calling webhook:', action.url);
2785
- const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
2786
- await fetch(action.url, {
2787
- method: action.method,
2788
- headers: {
2789
- 'Content-Type': 'application/json',
2790
- ...action.headers,
2791
- },
2792
- body,
2793
- });
2794
- }
2795
- /**
2796
- * Execute create task action
2797
- */
2798
- async executeCreateTask(action, data) {
2799
- logger.debug('Creating task:', action.title);
2800
- const dueDate = action.dueDays
2801
- ? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
2802
- : undefined;
2803
- await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
2804
- method: 'POST',
2805
- body: JSON.stringify({
2806
- title: this.replaceVariables(action.title, data),
2807
- description: action.description ? this.replaceVariables(action.description, data) : undefined,
2808
- priority: action.priority,
2809
- dueDate,
2810
- assignedTo: action.assignedTo,
2811
- relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
2812
- }),
2813
- });
2814
- }
2815
- /**
2816
- * Execute update contact action
2817
- */
2818
- async executeUpdateContact(action, data) {
2819
- const contactId = data.contactId || data._id;
2820
- if (!contactId) {
2821
- logger.warn('Cannot update contact: no contactId in data');
2822
- return;
2823
- }
2824
- logger.debug('Updating contact:', contactId);
2825
- await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2826
- method: 'PUT',
2827
- body: JSON.stringify(action.updates),
2828
- });
2829
- }
2830
- /**
2831
- * Replace variables in a string template
2832
- * Supports syntax like {{contact.email}}, {{opportunity.value}}
2833
- */
2834
- replaceVariables(template, data) {
2835
- return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
2836
- const value = this.getNestedValue(data, path.trim());
2837
- return value !== undefined ? String(value) : match;
2838
- });
2839
- }
2840
- /**
2841
- * Get nested value from object using dot notation
2842
- * Supports dynamic field access including custom fields
2843
- */
2844
- getNestedValue(obj, path) {
2845
- return path.split('.').reduce((current, key) => {
2846
- return current !== null && current !== undefined && typeof current === 'object'
2847
- ? current[key]
2848
- : undefined;
2849
- }, obj);
2850
- }
2851
- /**
2852
- * Extract all available field paths from a data object
2853
- * Useful for dynamic field discovery based on platform-specific attributes
2854
- * @param obj - The data object to extract fields from
2855
- * @param prefix - Internal use for nested paths
2856
- * @param maxDepth - Maximum depth to traverse (default: 3)
2857
- * @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
2858
- */
2859
- extractAvailableFields(obj, prefix = '', maxDepth = 3) {
2860
- if (maxDepth <= 0)
2861
- return [];
2862
- const fields = [];
2863
- for (const key in obj) {
2864
- if (!obj.hasOwnProperty(key))
2865
- continue;
2866
- const value = obj[key];
2867
- const fieldPath = prefix ? `${prefix}.${key}` : key;
2868
- fields.push(fieldPath);
2869
- // Recursively traverse nested objects
2870
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
2871
- const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
2872
- fields.push(...nestedFields);
2873
- }
2874
- }
2875
- return fields;
2876
- }
2877
- /**
2878
- * Get available fields from sample data
2879
- * Helps with dynamic field detection for platform-specific attributes
2880
- * @param sampleData - Sample data object to analyze
2881
- * @returns Array of available field paths
2882
- */
2883
- getAvailableFields(sampleData) {
2884
- return this.extractAvailableFields(sampleData);
2885
- }
2886
- // ============================================
2887
- // HELPER METHODS FOR COMMON PATTERNS
2888
- // ============================================
2889
- /**
2890
- * Create a simple email trigger
2891
- * Helper method for common use case
2892
- */
2893
- async createEmailTrigger(config) {
2894
- return this.createTrigger({
2895
- name: config.name,
2896
- eventType: config.eventType,
2897
- conditions: config.conditions,
2898
- actions: [
2899
- {
2900
- type: 'send_email',
2901
- to: config.to,
2902
- subject: config.subject,
2903
- body: config.body,
2904
- },
2905
- ],
2906
- isActive: true,
2907
- });
2908
- }
2909
- /**
2910
- * Create a task creation trigger
2911
- */
2912
- async createTaskTrigger(config) {
2913
- return this.createTrigger({
2914
- name: config.name,
2915
- eventType: config.eventType,
2916
- conditions: config.conditions,
2917
- actions: [
2918
- {
2919
- type: 'create_task',
2920
- title: config.taskTitle,
2921
- description: config.taskDescription,
2922
- priority: config.priority,
2923
- dueDays: config.dueDays,
2924
- },
2925
- ],
2926
- isActive: true,
2927
- });
2928
- }
2929
- /**
2930
- * Create a webhook trigger
2931
- */
2932
- async createWebhookTrigger(config) {
2933
- return this.createTrigger({
2934
- name: config.name,
2935
- eventType: config.eventType,
2936
- conditions: config.conditions,
2937
- actions: [
2938
- {
2939
- type: 'webhook',
2940
- url: config.webhookUrl,
2941
- method: config.method || 'POST',
2942
- },
2943
- ],
2944
- isActive: true,
2945
- });
2946
- }
2947
- }
2948
-
2949
- /**
2950
- * Clianta SDK - CRM API Client
2951
- * @see SDK_VERSION in core/config.ts
2952
- */
2953
- /**
2954
- * CRM API Client for managing contacts and opportunities
2955
- */
2956
- class CRMClient {
2957
- constructor(apiEndpoint, workspaceId, authToken, apiKey) {
2958
- this.apiEndpoint = apiEndpoint;
2959
- this.workspaceId = workspaceId;
2960
- this.authToken = authToken;
2961
- this.apiKey = apiKey;
2962
- this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
2963
- }
2964
- /**
2965
- * Set authentication token for API requests (user JWT)
2966
- */
2967
- setAuthToken(token) {
2968
- this.authToken = token;
2969
- this.apiKey = undefined;
2970
- this.triggers.setAuthToken(token);
2971
- }
2972
- /**
2973
- * Set workspace API key for server-to-server requests.
2974
- * Use this instead of setAuthToken when integrating from an external app.
2975
- */
2976
- setApiKey(key) {
2977
- this.apiKey = key;
2978
- this.authToken = undefined;
2979
- }
2980
- /**
2981
- * Validate required parameter exists
2982
- * @throws {Error} if value is null/undefined or empty string
2983
- */
2984
- validateRequired(param, value, methodName) {
2985
- if (value === null || value === undefined || value === '') {
2986
- throw new Error(`[CRMClient.${methodName}] ${param} is required`);
2987
- }
2988
- }
2989
- /**
2990
- * Make authenticated API request
2991
- */
2992
- async request(endpoint, options = {}) {
2993
- const url = `${this.apiEndpoint}${endpoint}`;
2994
- const headers = {
2995
- 'Content-Type': 'application/json',
2996
- ...(options.headers || {}),
2997
- };
2998
- if (this.apiKey) {
2999
- headers['X-Api-Key'] = this.apiKey;
3000
- }
3001
- else if (this.authToken) {
3002
- headers['Authorization'] = `Bearer ${this.authToken}`;
3003
- }
3004
- try {
3005
- const response = await fetch(url, {
3006
- ...options,
3007
- headers,
3008
- });
3009
- const data = await response.json();
3010
- if (!response.ok) {
3011
- return {
3012
- success: false,
3013
- error: data.message || 'Request failed',
3014
- status: response.status,
3015
- };
3016
- }
3017
- return {
3018
- success: true,
3019
- data: data.data || data,
3020
- status: response.status,
3021
- };
3022
- }
3023
- catch (error) {
3024
- return {
3025
- success: false,
3026
- error: error instanceof Error ? error.message : 'Network error',
3027
- status: 0,
3028
- };
3029
- }
3030
- }
3031
- // ============================================
3032
- // INBOUND EVENTS API (API-key authenticated)
3033
- // ============================================
3034
- /**
3035
- * Send an inbound event from an external app (e.g. user signup on client website).
3036
- * Requires the client to be initialized with an API key via setApiKey() or the constructor.
3037
- *
3038
- * The contact is upserted in the CRM and matching workflow automations fire automatically.
3039
- *
3040
- * @example
3041
- * const crm = new CRMClient('http://localhost:5000', 'WORKSPACE_ID');
3042
- * crm.setApiKey('mm_live_...');
3043
- *
3044
- * await crm.sendEvent({
3045
- * event: 'user.registered',
3046
- * contact: { email: 'alice@example.com', firstName: 'Alice' },
3047
- * data: { plan: 'free', signupSource: 'homepage' },
3048
- * });
3049
- */
3050
- async sendEvent(payload) {
3051
- const url = `${this.apiEndpoint}/api/public/events`;
3052
- const headers = { 'Content-Type': 'application/json' };
3053
- if (this.apiKey) {
3054
- headers['X-Api-Key'] = this.apiKey;
3055
- }
3056
- else if (this.authToken) {
3057
- headers['Authorization'] = `Bearer ${this.authToken}`;
3058
- }
3059
- try {
3060
- const response = await fetch(url, {
3061
- method: 'POST',
3062
- headers,
3063
- body: JSON.stringify(payload),
3064
- });
3065
- const data = await response.json();
3066
- if (!response.ok) {
3067
- return {
3068
- success: false,
3069
- contactCreated: false,
3070
- event: payload.event,
3071
- error: data.error || 'Request failed',
3072
- };
3073
- }
3074
- return {
3075
- success: data.success,
3076
- contactCreated: data.contactCreated,
3077
- contactId: data.contactId,
3078
- event: data.event,
3079
- };
3080
- }
3081
- catch (error) {
3082
- return {
3083
- success: false,
3084
- contactCreated: false,
3085
- event: payload.event,
3086
- error: error instanceof Error ? error.message : 'Network error',
3087
- };
3088
- }
3089
- }
3090
- // ============================================
3091
- // CONTACTS API
3092
- // ============================================
3093
- /**
3094
- * Get all contacts with pagination
3095
- */
3096
- async getContacts(params) {
3097
- const queryParams = new URLSearchParams();
3098
- if (params?.page)
3099
- queryParams.set('page', params.page.toString());
3100
- if (params?.limit)
3101
- queryParams.set('limit', params.limit.toString());
3102
- if (params?.search)
3103
- queryParams.set('search', params.search);
3104
- if (params?.status)
3105
- queryParams.set('status', params.status);
3106
- const query = queryParams.toString();
3107
- const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
3108
- return this.request(endpoint);
3109
- }
3110
- /**
3111
- * Get a single contact by ID
3112
- */
3113
- async getContact(contactId) {
3114
- this.validateRequired('contactId', contactId, 'getContact');
3115
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
3116
- }
3117
- /**
3118
- * Create a new contact
3119
- */
3120
- async createContact(contact) {
3121
- return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
3122
- method: 'POST',
3123
- body: JSON.stringify(contact),
3124
- });
3125
- }
3126
- /**
3127
- * Update an existing contact
3128
- */
3129
- async updateContact(contactId, updates) {
3130
- this.validateRequired('contactId', contactId, 'updateContact');
3131
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3132
- method: 'PUT',
3133
- body: JSON.stringify(updates),
3134
- });
3135
- }
3136
- /**
3137
- * Delete a contact
3138
- */
3139
- async deleteContact(contactId) {
3140
- this.validateRequired('contactId', contactId, 'deleteContact');
3141
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
3142
- method: 'DELETE',
3143
- });
3144
- }
3145
- // ============================================
3146
- // OPPORTUNITIES API
3147
- // ============================================
3148
- /**
3149
- * Get all opportunities with pagination
3150
- */
3151
- async getOpportunities(params) {
3152
- const queryParams = new URLSearchParams();
3153
- if (params?.page)
3154
- queryParams.set('page', params.page.toString());
3155
- if (params?.limit)
3156
- queryParams.set('limit', params.limit.toString());
3157
- if (params?.pipelineId)
3158
- queryParams.set('pipelineId', params.pipelineId);
3159
- if (params?.stageId)
3160
- queryParams.set('stageId', params.stageId);
3161
- const query = queryParams.toString();
3162
- const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
3163
- return this.request(endpoint);
3164
- }
3165
- /**
3166
- * Get a single opportunity by ID
3167
- */
3168
- async getOpportunity(opportunityId) {
3169
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
3170
- }
3171
- /**
3172
- * Create a new opportunity
3173
- */
3174
- async createOpportunity(opportunity) {
3175
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
3176
- method: 'POST',
3177
- body: JSON.stringify(opportunity),
3178
- });
3179
- }
3180
- /**
3181
- * Update an existing opportunity
3182
- */
3183
- async updateOpportunity(opportunityId, updates) {
3184
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3185
- method: 'PUT',
3186
- body: JSON.stringify(updates),
3187
- });
3188
- }
3189
- /**
3190
- * Delete an opportunity
3191
- */
3192
- async deleteOpportunity(opportunityId) {
3193
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
3194
- method: 'DELETE',
3195
- });
3196
- }
3197
- /**
3198
- * Move opportunity to a different stage
3199
- */
3200
- async moveOpportunity(opportunityId, stageId) {
3201
- return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
3202
- method: 'POST',
3203
- body: JSON.stringify({ stageId }),
3204
- });
3205
- }
3206
- // ============================================
3207
- // COMPANIES API
3208
- // ============================================
3209
- /**
3210
- * Get all companies with pagination
3211
- */
3212
- async getCompanies(params) {
3213
- const queryParams = new URLSearchParams();
3214
- if (params?.page)
3215
- queryParams.set('page', params.page.toString());
3216
- if (params?.limit)
3217
- queryParams.set('limit', params.limit.toString());
3218
- if (params?.search)
3219
- queryParams.set('search', params.search);
3220
- if (params?.status)
3221
- queryParams.set('status', params.status);
3222
- if (params?.industry)
3223
- queryParams.set('industry', params.industry);
3224
- const query = queryParams.toString();
3225
- const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
3226
- return this.request(endpoint);
3227
- }
3228
- /**
3229
- * Get a single company by ID
3230
- */
3231
- async getCompany(companyId) {
3232
- return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
3233
- }
3234
- /**
3235
- * Create a new company
3236
- */
3237
- async createCompany(company) {
3238
- return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
3239
- method: 'POST',
3240
- body: JSON.stringify(company),
3241
- });
3242
- }
3243
- /**
3244
- * Update an existing company
3245
- */
3246
- async updateCompany(companyId, updates) {
3247
- return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3248
- method: 'PUT',
3249
- body: JSON.stringify(updates),
3250
- });
3251
- }
3252
- /**
3253
- * Delete a company
3254
- */
3255
- async deleteCompany(companyId) {
3256
- return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
3257
- method: 'DELETE',
3258
- });
3259
- }
3260
- /**
3261
- * Get contacts belonging to a company
3262
- */
3263
- async getCompanyContacts(companyId, params) {
3264
- const queryParams = new URLSearchParams();
3265
- if (params?.page)
3266
- queryParams.set('page', params.page.toString());
3267
- if (params?.limit)
3268
- queryParams.set('limit', params.limit.toString());
3269
- const query = queryParams.toString();
3270
- const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
3271
- return this.request(endpoint);
3272
- }
3273
- /**
3274
- * Get deals/opportunities belonging to a company
3275
- */
3276
- async getCompanyDeals(companyId, params) {
3277
- const queryParams = new URLSearchParams();
3278
- if (params?.page)
3279
- queryParams.set('page', params.page.toString());
3280
- if (params?.limit)
3281
- queryParams.set('limit', params.limit.toString());
3282
- const query = queryParams.toString();
3283
- const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
3284
- return this.request(endpoint);
3285
- }
3286
- // ============================================
3287
- // PIPELINES API
3288
- // ============================================
3289
- /**
3290
- * Get all pipelines
3291
- */
3292
- async getPipelines() {
3293
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
3294
- }
3295
- /**
3296
- * Get a single pipeline by ID
3297
- */
3298
- async getPipeline(pipelineId) {
3299
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
3300
- }
3301
- /**
3302
- * Create a new pipeline
3303
- */
3304
- async createPipeline(pipeline) {
3305
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
3306
- method: 'POST',
3307
- body: JSON.stringify(pipeline),
3308
- });
3309
- }
3310
- /**
3311
- * Update an existing pipeline
3312
- */
3313
- async updatePipeline(pipelineId, updates) {
3314
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3315
- method: 'PUT',
3316
- body: JSON.stringify(updates),
3317
- });
3318
- }
3319
- /**
3320
- * Delete a pipeline
3321
- */
3322
- async deletePipeline(pipelineId) {
3323
- return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
3324
- method: 'DELETE',
3325
- });
3326
- }
3327
- // ============================================
3328
- // TASKS API
3329
- // ============================================
3330
- /**
3331
- * Get all tasks with pagination
3332
- */
3333
- async getTasks(params) {
3334
- const queryParams = new URLSearchParams();
3335
- if (params?.page)
3336
- queryParams.set('page', params.page.toString());
3337
- if (params?.limit)
3338
- queryParams.set('limit', params.limit.toString());
3339
- if (params?.status)
3340
- queryParams.set('status', params.status);
3341
- if (params?.priority)
3342
- queryParams.set('priority', params.priority);
3343
- if (params?.contactId)
3344
- queryParams.set('contactId', params.contactId);
3345
- if (params?.companyId)
3346
- queryParams.set('companyId', params.companyId);
3347
- if (params?.opportunityId)
3348
- queryParams.set('opportunityId', params.opportunityId);
3349
- const query = queryParams.toString();
3350
- const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
3351
- return this.request(endpoint);
3352
- }
3353
- /**
3354
- * Get a single task by ID
3355
- */
3356
- async getTask(taskId) {
3357
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
3358
- }
3359
- /**
3360
- * Create a new task
3361
- */
3362
- async createTask(task) {
3363
- return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
3364
- method: 'POST',
3365
- body: JSON.stringify(task),
3366
- });
3367
- }
3368
- /**
3369
- * Update an existing task
3370
- */
3371
- async updateTask(taskId, updates) {
3372
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3373
- method: 'PUT',
3374
- body: JSON.stringify(updates),
3375
- });
3376
- }
3377
- /**
3378
- * Mark a task as completed
3379
- */
3380
- async completeTask(taskId) {
3381
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
3382
- method: 'PATCH',
3383
- });
3384
- }
3385
- /**
3386
- * Delete a task
3387
- */
3388
- async deleteTask(taskId) {
3389
- return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
3390
- method: 'DELETE',
3391
- });
3392
- }
3393
- // ============================================
3394
- // ACTIVITIES API
3395
- // ============================================
3396
- /**
3397
- * Get activities for a contact
3398
- */
3399
- async getContactActivities(contactId, params) {
3400
- const queryParams = new URLSearchParams();
3401
- if (params?.page)
3402
- queryParams.set('page', params.page.toString());
3403
- if (params?.limit)
3404
- queryParams.set('limit', params.limit.toString());
3405
- if (params?.type)
3406
- queryParams.set('type', params.type);
3407
- const query = queryParams.toString();
3408
- const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3409
- return this.request(endpoint);
3410
- }
3411
- /**
3412
- * Get activities for an opportunity/deal
3413
- */
3414
- async getOpportunityActivities(opportunityId, params) {
3415
- const queryParams = new URLSearchParams();
3416
- if (params?.page)
3417
- queryParams.set('page', params.page.toString());
3418
- if (params?.limit)
3419
- queryParams.set('limit', params.limit.toString());
3420
- if (params?.type)
3421
- queryParams.set('type', params.type);
3422
- const query = queryParams.toString();
3423
- const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
3424
- return this.request(endpoint);
3425
- }
3426
- /**
3427
- * Create a new activity
3428
- */
3429
- async createActivity(activity) {
3430
- // Determine the correct endpoint based on related entity
3431
- let endpoint;
3432
- if (activity.opportunityId) {
3433
- endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
3434
- }
3435
- else if (activity.contactId) {
3436
- endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
3437
- }
3438
- else {
3439
- endpoint = `/api/workspaces/${this.workspaceId}/activities`;
3440
- }
3441
- return this.request(endpoint, {
3442
- method: 'POST',
3443
- body: JSON.stringify(activity),
3444
- });
3445
- }
3446
- /**
3447
- * Update an existing activity
3448
- */
3449
- async updateActivity(activityId, updates) {
3450
- return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3451
- method: 'PATCH',
3452
- body: JSON.stringify(updates),
3453
- });
3454
- }
3455
- /**
3456
- * Delete an activity
3457
- */
3458
- async deleteActivity(activityId) {
3459
- return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
3460
- method: 'DELETE',
3461
- });
3462
- }
3463
- /**
3464
- * Log a call activity
3465
- */
3466
- async logCall(data) {
3467
- return this.createActivity({
3468
- type: 'call',
3469
- title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
3470
- direction: data.direction,
3471
- duration: data.duration,
3472
- outcome: data.outcome,
3473
- description: data.notes,
3474
- contactId: data.contactId,
3475
- opportunityId: data.opportunityId,
3476
- });
3477
- }
3478
- /**
3479
- * Log a meeting activity
3480
- */
3481
- async logMeeting(data) {
3482
- return this.createActivity({
3483
- type: 'meeting',
3484
- title: data.title,
3485
- duration: data.duration,
3486
- outcome: data.outcome,
3487
- description: data.notes,
3488
- contactId: data.contactId,
3489
- opportunityId: data.opportunityId,
3490
- });
3491
- }
3492
- /**
3493
- * Add a note to a contact or opportunity
3494
- */
3495
- async addNote(data) {
3496
- return this.createActivity({
3497
- type: 'note',
3498
- title: 'Note',
3499
- description: data.content,
3500
- contactId: data.contactId,
3501
- opportunityId: data.opportunityId,
3502
- });
3503
- }
3504
- // ============================================
3505
- // EMAIL TEMPLATES API
3506
- // ============================================
3507
- /**
3508
- * Get all email templates
3509
- */
3510
- async getEmailTemplates(params) {
3511
- const queryParams = new URLSearchParams();
3512
- if (params?.page)
3513
- queryParams.set('page', params.page.toString());
3514
- if (params?.limit)
3515
- queryParams.set('limit', params.limit.toString());
3516
- const query = queryParams.toString();
3517
- const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
3518
- return this.request(endpoint);
3519
- }
3520
- /**
3521
- * Get a single email template by ID
3522
- */
3523
- async getEmailTemplate(templateId) {
3524
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
3525
- }
3526
- /**
3527
- * Create a new email template
3528
- */
3529
- async createEmailTemplate(template) {
3530
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
3531
- method: 'POST',
3532
- body: JSON.stringify(template),
3533
- });
3534
- }
3535
- /**
3536
- * Update an email template
3537
- */
3538
- async updateEmailTemplate(templateId, updates) {
3539
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3540
- method: 'PUT',
3541
- body: JSON.stringify(updates),
3542
- });
3543
- }
3544
- /**
3545
- * Delete an email template
3546
- */
3547
- async deleteEmailTemplate(templateId) {
3548
- return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3549
- method: 'DELETE',
3550
- });
3551
- }
3552
- /**
3553
- * Send an email using a template
3554
- */
3555
- async sendEmail(data) {
3556
- return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3557
- method: 'POST',
3558
- body: JSON.stringify(data),
3559
- });
3560
- }
3561
- // ============================================
3562
- // READ-BACK / DATA RETRIEVAL API
3563
- // ============================================
3564
- /**
3565
- * Get a contact by email address.
3566
- * Returns the first matching contact from a search query.
3567
- */
3568
- async getContactByEmail(email) {
3569
- this.validateRequired('email', email, 'getContactByEmail');
3570
- const queryParams = new URLSearchParams({ search: email, limit: '1' });
3571
- return this.request(`/api/workspaces/${this.workspaceId}/contacts?${queryParams.toString()}`);
3572
- }
3573
- /**
3574
- * Get activity timeline for a contact
3575
- */
3576
- async getContactActivity(contactId, params) {
3577
- this.validateRequired('contactId', contactId, 'getContactActivity');
3578
- const queryParams = new URLSearchParams();
3579
- if (params?.page)
3580
- queryParams.set('page', params.page.toString());
3581
- if (params?.limit)
3582
- queryParams.set('limit', params.limit.toString());
3583
- if (params?.type)
3584
- queryParams.set('type', params.type);
3585
- if (params?.startDate)
3586
- queryParams.set('startDate', params.startDate);
3587
- if (params?.endDate)
3588
- queryParams.set('endDate', params.endDate);
3589
- const query = queryParams.toString();
3590
- const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
3591
- return this.request(endpoint);
3592
- }
3593
- /**
3594
- * Get engagement metrics for a contact (via their linked visitor data)
3595
- */
3596
- async getContactEngagement(contactId) {
3597
- this.validateRequired('contactId', contactId, 'getContactEngagement');
3598
- return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}/engagement`);
3599
- }
3600
- /**
3601
- * Get a full timeline for a contact including events, activities, and opportunities
3602
- */
3603
- async getContactTimeline(contactId, params) {
3604
- this.validateRequired('contactId', contactId, 'getContactTimeline');
3605
- const queryParams = new URLSearchParams();
3606
- if (params?.page)
3607
- queryParams.set('page', params.page.toString());
3608
- if (params?.limit)
3609
- queryParams.set('limit', params.limit.toString());
3610
- const query = queryParams.toString();
3611
- const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/timeline${query ? `?${query}` : ''}`;
3612
- return this.request(endpoint);
3613
- }
3614
- /**
3615
- * Search contacts with advanced filters
3616
- */
3617
- async searchContacts(query, filters) {
3618
- const queryParams = new URLSearchParams();
3619
- queryParams.set('search', query);
3620
- if (filters?.status)
3621
- queryParams.set('status', filters.status);
3622
- if (filters?.lifecycleStage)
3623
- queryParams.set('lifecycleStage', filters.lifecycleStage);
3624
- if (filters?.source)
3625
- queryParams.set('source', filters.source);
3626
- if (filters?.tags)
3627
- queryParams.set('tags', filters.tags.join(','));
3628
- if (filters?.page)
3629
- queryParams.set('page', filters.page.toString());
3630
- if (filters?.limit)
3631
- queryParams.set('limit', filters.limit.toString());
3632
- const qs = queryParams.toString();
3633
- const endpoint = `/api/workspaces/${this.workspaceId}/contacts${qs ? `?${qs}` : ''}`;
3634
- return this.request(endpoint);
3635
- }
3636
- // ============================================
3637
- // WEBHOOK MANAGEMENT API
3638
- // ============================================
3639
- /**
3640
- * List all webhook subscriptions
3641
- */
3642
- async listWebhooks(params) {
3643
- const queryParams = new URLSearchParams();
3644
- if (params?.page)
3645
- queryParams.set('page', params.page.toString());
3646
- if (params?.limit)
3647
- queryParams.set('limit', params.limit.toString());
3648
- const query = queryParams.toString();
3649
- return this.request(`/api/workspaces/${this.workspaceId}/webhooks${query ? `?${query}` : ''}`);
3650
- }
3651
- /**
3652
- * Create a new webhook subscription
3653
- */
3654
- async createWebhook(data) {
3655
- return this.request(`/api/workspaces/${this.workspaceId}/webhooks`, {
3656
- method: 'POST',
3657
- body: JSON.stringify(data),
3658
- });
3659
- }
3660
- /**
3661
- * Delete a webhook subscription
3662
- */
3663
- async deleteWebhook(webhookId) {
3664
- this.validateRequired('webhookId', webhookId, 'deleteWebhook');
3665
- return this.request(`/api/workspaces/${this.workspaceId}/webhooks/${webhookId}`, {
3666
- method: 'DELETE',
3667
- });
3668
- }
3669
- // ============================================
3670
- // EVENT TRIGGERS API (delegated to triggers manager)
3671
- // ============================================
3672
- /**
3673
- * Get all event triggers
3674
- */
3675
- async getEventTriggers() {
3676
- return this.triggers.getTriggers();
3677
- }
3678
- /**
3679
- * Create a new event trigger
3680
- */
3681
- async createEventTrigger(trigger) {
3682
- return this.triggers.createTrigger(trigger);
3683
- }
3684
- /**
3685
- * Update an event trigger
3686
- */
3687
- async updateEventTrigger(triggerId, updates) {
3688
- return this.triggers.updateTrigger(triggerId, updates);
3689
- }
3690
- /**
3691
- * Delete an event trigger
3692
- */
3693
- async deleteEventTrigger(triggerId) {
3694
- return this.triggers.deleteTrigger(triggerId);
3695
- }
3696
- }
3697
-
3698
2513
  /**
3699
2514
  * Clianta SDK - Main Tracker Class
3700
2515
  * @see SDK_VERSION in core/config.ts
@@ -3708,10 +2523,16 @@ class Tracker {
3708
2523
  this.isInitialized = false;
3709
2524
  /** contactId after a successful identify() call */
3710
2525
  this.contactId = null;
2526
+ /** groupId after a successful group() call */
2527
+ this.groupId = null;
3711
2528
  /** Pending identify retry on next flush */
3712
2529
  this.pendingIdentify = null;
3713
2530
  /** Registered event schemas for validation */
3714
2531
  this.eventSchemas = new Map();
2532
+ /** Event middleware pipeline */
2533
+ this.middlewares = [];
2534
+ /** Ready callbacks */
2535
+ this.readyCallbacks = [];
3715
2536
  if (!workspaceId) {
3716
2537
  throw new Error('[Clianta] Workspace ID is required');
3717
2538
  }
@@ -3742,15 +2563,22 @@ class Tracker {
3742
2563
  typeof window !== 'undefined' &&
3743
2564
  !window.location.hostname.includes('localhost') &&
3744
2565
  !window.location.hostname.includes('127.0.0.1')) {
3745
- logger.warn('apiEndpoint uses HTTP — events and visitor data will be sent unencrypted. Use HTTPS in production.');
3746
- }
3747
- if (this.config.apiKey && typeof window !== 'undefined') {
3748
- logger.warn('API key is exposed in client-side code. Use API keys only in server-side (Node.js) environments.');
2566
+ logger.warn('apiEndpoint uses HTTP — events will be sent unencrypted. Use HTTPS in production.');
3749
2567
  }
3750
2568
  // Initialize plugins
3751
2569
  this.initPlugins();
3752
2570
  this.isInitialized = true;
3753
2571
  logger.info('SDK initialized successfully');
2572
+ // Fire ready callbacks
2573
+ for (const cb of this.readyCallbacks) {
2574
+ try {
2575
+ cb();
2576
+ }
2577
+ catch (e) {
2578
+ logger.error('onReady callback error:', e);
2579
+ }
2580
+ }
2581
+ this.readyCallbacks = [];
3754
2582
  }
3755
2583
  /**
3756
2584
  * Create visitor ID based on storage mode
@@ -3863,6 +2691,10 @@ class Tracker {
3863
2691
  if (this.contactId) {
3864
2692
  event.contactId = this.contactId;
3865
2693
  }
2694
+ // Attach groupId if known (from a prior group() call)
2695
+ if (this.groupId) {
2696
+ event.groupId = this.groupId;
2697
+ }
3866
2698
  // Validate event against registered schema (debug mode only)
3867
2699
  this.validateEventSchema(eventType, properties);
3868
2700
  // Check consent before tracking
@@ -3876,8 +2708,11 @@ class Tracker {
3876
2708
  logger.debug('Event dropped (no consent):', eventName);
3877
2709
  return;
3878
2710
  }
3879
- this.queue.push(event);
3880
- logger.debug('Event tracked:', eventName, properties);
2711
+ // Run event through middleware pipeline
2712
+ this.runMiddleware(event, () => {
2713
+ this.queue.push(event);
2714
+ logger.debug('Event tracked:', eventName, properties);
2715
+ });
3881
2716
  }
3882
2717
  /**
3883
2718
  * Track a page view
@@ -3925,123 +2760,176 @@ class Tracker {
3925
2760
  }
3926
2761
  }
3927
2762
  /**
3928
- * Send a server-side inbound event via the API key endpoint.
3929
- * Convenience proxy to CRMClient.sendEvent() — requires apiKey in config.
2763
+ * Retry pending identify call
3930
2764
  */
3931
- async sendEvent(payload) {
3932
- const apiKey = this.config.apiKey;
3933
- if (!apiKey) {
3934
- logger.error('sendEvent() requires an apiKey in the SDK config');
3935
- return { success: false, contactCreated: false, event: payload.event, error: 'No API key configured' };
3936
- }
3937
- const client = new CRMClient(this.config.apiEndpoint, this.workspaceId, undefined, apiKey);
3938
- return client.sendEvent(payload);
2765
+ async retryPendingIdentify() {
2766
+ if (!this.pendingIdentify)
2767
+ return;
2768
+ const { email, traits } = this.pendingIdentify;
2769
+ this.pendingIdentify = null;
2770
+ await this.identify(email, traits);
3939
2771
  }
3940
2772
  /**
3941
- * Get the current visitor's profile from the CRM.
3942
- * Returns visitor data and linked contact info if identified.
3943
- * Only returns data for the current visitor (privacy-safe for frontend).
2773
+ * Update consent state
3944
2774
  */
3945
- async getVisitorProfile() {
3946
- if (!this.isInitialized) {
3947
- logger.warn('SDK not initialized');
3948
- return null;
3949
- }
3950
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/profile`);
3951
- if (result.success && result.data) {
3952
- logger.debug('Visitor profile fetched:', result.data);
3953
- return result.data;
3954
- }
3955
- logger.warn('Failed to fetch visitor profile:', result.error);
3956
- return null;
2775
+ consent(state) {
2776
+ this.consentManager.update(state);
3957
2777
  }
3958
2778
  /**
3959
- * Get the current visitor's recent activity/events.
3960
- * Returns paginated list of tracking events for this visitor.
2779
+ * Get current consent state
3961
2780
  */
3962
- async getVisitorActivity(options) {
3963
- if (!this.isInitialized) {
3964
- logger.warn('SDK not initialized');
3965
- return null;
3966
- }
3967
- const params = {};
3968
- if (options?.page)
3969
- params.page = options.page.toString();
3970
- if (options?.limit)
3971
- params.limit = options.limit.toString();
3972
- if (options?.eventType)
3973
- params.eventType = options.eventType;
3974
- if (options?.startDate)
3975
- params.startDate = options.startDate;
3976
- if (options?.endDate)
3977
- params.endDate = options.endDate;
3978
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/activity`, params);
3979
- if (result.success && result.data) {
3980
- return result.data;
3981
- }
3982
- logger.warn('Failed to fetch visitor activity:', result.error);
3983
- return null;
2781
+ getConsentState() {
2782
+ return this.consentManager.getState();
3984
2783
  }
3985
2784
  /**
3986
- * Get a summarized journey timeline for the current visitor.
3987
- * Includes top pages, sessions, time spent, and recent activities.
2785
+ * Toggle debug mode
3988
2786
  */
3989
- async getVisitorTimeline() {
3990
- if (!this.isInitialized) {
3991
- logger.warn('SDK not initialized');
3992
- return null;
3993
- }
3994
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/timeline`);
3995
- if (result.success && result.data) {
3996
- return result.data;
2787
+ debug(enabled) {
2788
+ logger.enabled = enabled;
2789
+ logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
2790
+ }
2791
+ // ============================================
2792
+ // GROUP, ALIAS, SCREEN
2793
+ // ============================================
2794
+ /**
2795
+ * Associate the current visitor with a group (company/account).
2796
+ * The groupId will be attached to all subsequent track() calls.
2797
+ */
2798
+ group(groupId, traits = {}) {
2799
+ if (!groupId) {
2800
+ logger.warn('groupId is required for group()');
2801
+ return;
3997
2802
  }
3998
- logger.warn('Failed to fetch visitor timeline:', result.error);
3999
- return null;
2803
+ this.groupId = groupId;
2804
+ logger.info('Visitor grouped:', groupId);
2805
+ this.track('group', 'Group Identified', {
2806
+ groupId,
2807
+ ...traits,
2808
+ });
4000
2809
  }
4001
2810
  /**
4002
- * Get engagement metrics for the current visitor.
4003
- * Includes time on site, page views, bounce rate, and engagement score.
2811
+ * Merge two visitor identities.
2812
+ * Links `previousId` (typically the anonymous visitor) to `newId` (the known user).
2813
+ * If `previousId` is omitted, the current visitorId is used.
4004
2814
  */
4005
- async getVisitorEngagement() {
4006
- if (!this.isInitialized) {
4007
- logger.warn('SDK not initialized');
4008
- return null;
2815
+ async alias(newId, previousId) {
2816
+ if (!newId) {
2817
+ logger.warn('newId is required for alias()');
2818
+ return false;
2819
+ }
2820
+ const prevId = previousId || this.visitorId;
2821
+ logger.info('Aliasing visitor:', { from: prevId, to: newId });
2822
+ try {
2823
+ const url = `${this.config.apiEndpoint}/api/public/track/alias`;
2824
+ const response = await fetch(url, {
2825
+ method: 'POST',
2826
+ headers: { 'Content-Type': 'application/json' },
2827
+ body: JSON.stringify({
2828
+ workspaceId: this.workspaceId,
2829
+ previousId: prevId,
2830
+ newId,
2831
+ }),
2832
+ });
2833
+ if (response.ok) {
2834
+ logger.info('Alias successful');
2835
+ return true;
2836
+ }
2837
+ logger.error('Alias failed:', response.status);
2838
+ return false;
4009
2839
  }
4010
- const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/engagement`);
4011
- if (result.success && result.data) {
4012
- return result.data;
2840
+ catch (error) {
2841
+ logger.error('Alias request failed:', error);
2842
+ return false;
4013
2843
  }
4014
- logger.warn('Failed to fetch visitor engagement:', result.error);
4015
- return null;
4016
2844
  }
4017
2845
  /**
4018
- * Retry pending identify call
2846
+ * Track a screen view (for mobile-first PWAs and SPAs).
2847
+ * Similar to page() but semantically for app screens.
4019
2848
  */
4020
- async retryPendingIdentify() {
4021
- if (!this.pendingIdentify)
4022
- return;
4023
- const { email, traits } = this.pendingIdentify;
4024
- this.pendingIdentify = null;
4025
- await this.identify(email, traits);
2849
+ screen(name, properties = {}) {
2850
+ this.track('screen_view', name, {
2851
+ ...properties,
2852
+ screenName: name,
2853
+ });
4026
2854
  }
2855
+ // ============================================
2856
+ // MIDDLEWARE
2857
+ // ============================================
4027
2858
  /**
4028
- * Update consent state
2859
+ * Register event middleware.
2860
+ * Middleware functions receive the event and a `next` callback.
2861
+ * Call `next()` to pass the event through, or don't call it to drop the event.
2862
+ *
2863
+ * @example
2864
+ * tracker.use((event, next) => {
2865
+ * // Strip PII from events
2866
+ * delete event.properties.email;
2867
+ * next(); // pass it through
2868
+ * });
4029
2869
  */
4030
- consent(state) {
4031
- this.consentManager.update(state);
2870
+ use(middleware) {
2871
+ this.middlewares.push(middleware);
2872
+ logger.debug('Middleware registered');
4032
2873
  }
4033
2874
  /**
4034
- * Get current consent state
2875
+ * Run event through the middleware pipeline.
2876
+ * Executes each middleware in order; if any skips `next()`, the event is dropped.
4035
2877
  */
4036
- getConsentState() {
4037
- return this.consentManager.getState();
2878
+ runMiddleware(event, finalCallback) {
2879
+ if (this.middlewares.length === 0) {
2880
+ finalCallback();
2881
+ return;
2882
+ }
2883
+ let index = 0;
2884
+ const middlewares = this.middlewares;
2885
+ const next = () => {
2886
+ index++;
2887
+ if (index < middlewares.length) {
2888
+ try {
2889
+ middlewares[index](event, next);
2890
+ }
2891
+ catch (e) {
2892
+ logger.error('Middleware error:', e);
2893
+ finalCallback();
2894
+ }
2895
+ }
2896
+ else {
2897
+ finalCallback();
2898
+ }
2899
+ };
2900
+ try {
2901
+ middlewares[0](event, next);
2902
+ }
2903
+ catch (e) {
2904
+ logger.error('Middleware error:', e);
2905
+ finalCallback();
2906
+ }
4038
2907
  }
2908
+ // ============================================
2909
+ // LIFECYCLE
2910
+ // ============================================
4039
2911
  /**
4040
- * Toggle debug mode
2912
+ * Register a callback to be invoked when the SDK is fully initialized.
2913
+ * If already initialized, the callback fires immediately.
4041
2914
  */
4042
- debug(enabled) {
4043
- logger.enabled = enabled;
4044
- logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
2915
+ onReady(callback) {
2916
+ if (this.isInitialized) {
2917
+ try {
2918
+ callback();
2919
+ }
2920
+ catch (e) {
2921
+ logger.error('onReady callback error:', e);
2922
+ }
2923
+ }
2924
+ else {
2925
+ this.readyCallbacks.push(callback);
2926
+ }
2927
+ }
2928
+ /**
2929
+ * Check if the SDK is fully initialized and ready.
2930
+ */
2931
+ isReady() {
2932
+ return this.isInitialized;
4045
2933
  }
4046
2934
  /**
4047
2935
  * Register a schema for event validation.
@@ -4280,7 +3168,12 @@ class Tracker {
4280
3168
 
4281
3169
  /**
4282
3170
  * Clianta SDK
4283
- * Professional CRM and tracking SDK for lead generation
3171
+ * Client-side tracking SDK for CRM — tracks visitors, identifies contacts,
3172
+ * captures forms, and writes CRM data from client websites.
3173
+ *
3174
+ * This SDK is designed to run on CLIENT WEBSITES (React, Next.js, Vue, etc.)
3175
+ * It only SENDS data to your CRM — it never reads CRM data back.
3176
+ *
4284
3177
  * @see SDK_VERSION in core/config.ts
4285
3178
  */
4286
3179
  // Global instance cache
@@ -4322,29 +3215,55 @@ function clianta(workspaceId, config) {
4322
3215
  globalInstance = new Tracker(workspaceId, config);
4323
3216
  return globalInstance;
4324
3217
  }
4325
- // Attach to window for <script> usage
3218
+ // Attach to window for <script> tag usage
4326
3219
  if (typeof window !== 'undefined') {
4327
3220
  window.clianta = clianta;
4328
3221
  window.Clianta = {
4329
3222
  clianta,
4330
3223
  Tracker,
4331
- CRMClient,
4332
3224
  ConsentManager,
4333
- EventTriggersManager,
4334
3225
  };
4335
3226
  }
4336
3227
 
4337
3228
  /**
4338
3229
  * Clianta SDK - React Integration
4339
3230
  *
4340
- * Provides CliantaProvider component for easy React/Next.js integration
4341
- * using the clianta.config.ts pattern.
3231
+ * Provides CliantaProvider component (with ErrorBoundary) for easy
3232
+ * React/Next.js integration using the clianta.config.ts pattern.
3233
+ */
3234
+ const CliantaContext = react.createContext({
3235
+ tracker: null,
3236
+ isReady: false,
3237
+ });
3238
+ /**
3239
+ * Internal ErrorBoundary to prevent SDK errors from crashing the host app.
3240
+ * Catches render-time errors in the provider tree.
4342
3241
  */
4343
- // Context for accessing tracker throughout the app
4344
- const CliantaContext = react.createContext(null);
3242
+ class CliantaErrorBoundary extends react.Component {
3243
+ constructor(props) {
3244
+ super(props);
3245
+ this.state = { hasError: false };
3246
+ }
3247
+ static getDerivedStateFromError() {
3248
+ return { hasError: true };
3249
+ }
3250
+ componentDidCatch(error, errorInfo) {
3251
+ console.error('[Clianta] SDK error caught by ErrorBoundary:', error);
3252
+ this.props.onError?.(error, errorInfo);
3253
+ }
3254
+ render() {
3255
+ if (this.state.hasError) {
3256
+ // Render children anyway — SDK failure shouldn't break the host UI
3257
+ return this.props.fallback ?? this.props.children;
3258
+ }
3259
+ return this.props.children;
3260
+ }
3261
+ }
4345
3262
  /**
4346
3263
  * CliantaProvider - Wrap your app to enable tracking
4347
3264
  *
3265
+ * Includes an ErrorBoundary so SDK failures never crash the host app.
3266
+ *
4348
3267
  * @example
4349
3268
  * // In clianta.config.ts:
4350
3269
  * import { CliantaConfig } from '@clianta/sdk';
@@ -4365,8 +3284,9 @@ const CliantaContext = react.createContext(null);
4365
3284
  * {children}
4366
3285
  * </CliantaProvider>
4367
3286
  */
4368
- function CliantaProvider({ config, children }) {
3287
+ function CliantaProvider({ config, children, onError }) {
4369
3288
  const [tracker, setTracker] = react.useState(null);
3289
+ const [isReady, setIsReady] = react.useState(false);
4370
3290
  // Stable ref to projectId — the only value that truly identifies the tracker
4371
3291
  const projectIdRef = react.useRef(config.projectId);
4372
3292
  react.useEffect(() => {
@@ -4380,18 +3300,29 @@ function CliantaProvider({ config, children }) {
4380
3300
  if (projectIdRef.current !== projectId) {
4381
3301
  projectIdRef.current = projectId;
4382
3302
  }
4383
- // Extract projectId (handled separately) and pass rest as options
4384
- const { projectId: _, ...options } = config;
4385
- const instance = clianta(projectId, options);
4386
- setTracker(instance);
3303
+ try {
3304
+ // Extract projectId (handled separately) and pass rest as options
3305
+ const { projectId: _, ...options } = config;
3306
+ const instance = clianta(projectId, options);
3307
+ setTracker(instance);
3308
+ setIsReady(true);
3309
+ }
3310
+ catch (error) {
3311
+ console.error('[Clianta] Failed to initialize SDK:', error);
3312
+ onError?.(error, { componentStack: '' });
3313
+ }
4387
3314
  // Cleanup: flush pending events on unmount
4388
3315
  return () => {
4389
- instance?.flush();
3316
+ tracker?.flush();
3317
+ setIsReady(false);
4390
3318
  };
4391
3319
  // eslint-disable-next-line react-hooks/exhaustive-deps
4392
3320
  }, [config.projectId]);
4393
- return (jsxRuntime.jsx(CliantaContext.Provider, { value: tracker, children: children }));
3321
+ return (jsxRuntime.jsx(CliantaErrorBoundary, { onError: onError, children: jsxRuntime.jsx(CliantaContext.Provider, { value: { tracker, isReady }, children: children }) }));
4394
3322
  }
3323
+ // ============================================
3324
+ // HOOKS
3325
+ // ============================================
4395
3326
  /**
4396
3327
  * useClianta - Hook to access tracker in any component
4397
3328
  *
@@ -4400,7 +3331,21 @@ function CliantaProvider({ config, children }) {
4400
3331
  * tracker?.track('button_click', 'CTA Button');
4401
3332
  */
4402
3333
  function useClianta() {
4403
- return react.useContext(CliantaContext);
3334
+ const { tracker } = react.useContext(CliantaContext);
3335
+ return tracker;
3336
+ }
3337
+ /**
3338
+ * useCliantaReady - Hook to check if SDK is initialized
3339
+ *
3340
+ * @example
3341
+ * const { isReady, tracker } = useCliantaReady();
3342
+ * if (isReady) {
3343
+ * tracker.track('purchase', 'Order', { value: 99 });
3344
+ * }
3345
+ */
3346
+ function useCliantaReady() {
3347
+ const { tracker, isReady } = react.useContext(CliantaContext);
3348
+ return { isReady, tracker };
4404
3349
  }
4405
3350
  /**
4406
3351
  * useCliantaTrack - Convenience hook for tracking events
@@ -4418,5 +3363,6 @@ function useCliantaTrack() {
4418
3363
 
4419
3364
  exports.CliantaProvider = CliantaProvider;
4420
3365
  exports.useClianta = useClianta;
3366
+ exports.useCliantaReady = useCliantaReady;
4421
3367
  exports.useCliantaTrack = useCliantaTrack;
4422
3368
  //# sourceMappingURL=react.cjs.js.map