releasebird-javascript-sdk 1.0.90 → 1.0.92

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,19 +1,22 @@
1
1
  import RbirdSessionManager from "./RbirdSessionManager";
2
+ import RbirdWebsiteWidget from "./RbirdWebsiteWidget";
2
3
  import { RbirdBannerManager } from "./RbirdBannerManager";
3
4
  import { RbirdFormManager } from "./RbirdFormManager";
4
5
  import { RbirdSurveyManager } from "./RbirdSurveyManager";
5
6
 
6
7
  /**
7
- * Manages automations and their execution in the SDK.
8
- * Automations are event-driven workflows that can trigger actions
9
- * like showing banners, forms, surveys, or sending chat messages.
8
+ * Manages automations by sending events to the backend and executing commands.
9
+ * The actual workflow execution happens on the backend - this manager handles:
10
+ * - Sending events to the backend
11
+ * - Polling for pending commands
12
+ * - Executing UI commands (show banner, form, survey, send chat)
10
13
  */
11
14
  export class RbirdAutomationManager {
12
15
  static instance = null;
13
- automations = [];
14
16
  apiKey = null;
15
17
  firstSeenTimestamp = null;
16
- executedAutomations = new Set(); // Track which automations have been executed
18
+ pollingInterval = null;
19
+ isPolling = false;
17
20
 
18
21
  static getInstance() {
19
22
  if (!this.instance) {
@@ -30,14 +33,36 @@ export class RbirdAutomationManager {
30
33
  this.apiKey = apiKey;
31
34
  this.firstSeenTimestamp = this.getFirstSeenTimestamp();
32
35
 
33
- // Load automations from backend
34
- await this.loadAutomations();
35
-
36
36
  // Register event handlers
37
37
  this.registerEventHandlers();
38
38
 
39
- // Check for first_seen triggers immediately
40
- this.checkFirstSeenTriggers();
39
+ // Listen for WebSocket notifications from widget iframe
40
+ this.setupWebSocketListener();
41
+
42
+ // Start command polling (reduced frequency - WebSocket is primary)
43
+ this.startCommandPolling();
44
+
45
+ // Send first_seen event
46
+ await this.sendEvent('first_seen', {
47
+ timeSinceFirstSeen: (Date.now() - this.firstSeenTimestamp) / 1000
48
+ });
49
+
50
+ console.log('[Rbird] Automation manager initialized');
51
+ }
52
+
53
+ /**
54
+ * Setup listener for WebSocket notifications from widget iframe.
55
+ * This allows instant command execution when the backend pushes a notification.
56
+ */
57
+ setupWebSocketListener() {
58
+ window.addEventListener('message', (event) => {
59
+ // Handle automation command notification from widget
60
+ if (event.data === 'automationCommand') {
61
+ console.log('[Rbird] Received WebSocket automation notification');
62
+ // Immediately fetch and execute pending commands
63
+ this.pollCommands();
64
+ }
65
+ });
41
66
  }
42
67
 
43
68
  /**
@@ -53,297 +78,253 @@ export class RbirdAutomationManager {
53
78
  return now;
54
79
  }
55
80
 
56
- /**
57
- * Load active automations from the backend
58
- */
59
- async loadAutomations() {
60
- try {
61
- const sessionManager = RbirdSessionManager.getInstance();
62
- const baseUrl = sessionManager.apiUrl || 'https://api.releasebird.com';
63
-
64
- const response = await fetch(`${baseUrl}/papi/ewidget/automations`, {
65
- method: 'GET',
66
- headers: {
67
- 'Content-Type': 'application/json',
68
- 'apiKey': this.apiKey,
69
- 'languageCode': navigator.language?.split('-')[0] || 'en',
70
- },
71
- });
72
-
73
- if (response.ok) {
74
- this.automations = await response.json();
75
- console.log('[Rbird] Loaded automations:', this.automations.length);
76
- }
77
- } catch (error) {
78
- console.error('[Rbird] Error loading automations:', error);
79
- }
80
- }
81
-
82
81
  /**
83
82
  * Register event handlers for automation triggers
84
83
  */
85
84
  registerEventHandlers() {
86
- // Listen for page visits
87
- window.addEventListener('popstate', () => this.handleEvent('page_visit'));
85
+ // Listen for page visits (navigation)
86
+ window.addEventListener('popstate', () => this.sendPageVisitEvents());
87
+
88
+ // Also track initial page load
89
+ this.sendPageVisitEvents();
88
90
 
89
91
  // Listen for identify events
90
92
  document.addEventListener('rbird:identify', (e) => {
91
- this.handleEvent('user_identified', e.detail);
93
+ this.sendEvent('user_identified', e.detail);
94
+ // Also send user_page_visit when user is identified
95
+ this.sendEvent('user_page_visit');
92
96
  });
93
97
 
94
98
  // Listen for form submissions
95
99
  document.addEventListener('rbird:form_submitted', (e) => {
96
- this.handleEvent('form_submitted', e.detail);
100
+ this.sendEvent('form_submitted', e.detail);
97
101
  });
98
102
 
99
103
  // Listen for survey completions
100
104
  document.addEventListener('rbird:survey_completed', (e) => {
101
- this.handleEvent('survey_completed', e.detail);
105
+ this.sendEvent('survey_completed', e.detail);
102
106
  });
103
107
 
104
108
  // Listen for custom events
105
109
  document.addEventListener('rbird:track', (e) => {
106
- this.handleEvent(e.detail?.eventName, e.detail?.data);
110
+ this.sendEvent(e.detail?.eventName, e.detail?.data);
107
111
  });
108
112
  }
109
113
 
110
114
  /**
111
- * Check for first_seen triggers on initialization
115
+ * Send page visit events based on user identification status.
116
+ * Sends:
117
+ * - page_visit: always (for backward compatibility)
118
+ * - visitor_page_visit: if user is NOT identified (anonymous)
119
+ * - user_page_visit: if user IS identified
112
120
  */
113
- checkFirstSeenTriggers() {
114
- const timeSinceFirstSeen = (Date.now() - this.firstSeenTimestamp) / 1000; // in seconds
115
-
116
- this.automations.forEach((automation) => {
117
- const triggerNode = this.findTriggerNode(automation);
118
- if (triggerNode && triggerNode.data?.eventType === 'first_seen') {
119
- this.evaluateAndExecute(automation, triggerNode, { timeSinceFirstSeen });
120
- }
121
- });
121
+ sendPageVisitEvents() {
122
+ // Always send generic page_visit for backward compatibility
123
+ this.sendEvent('page_visit');
124
+
125
+ // Send specific event based on identification status
126
+ const peopleId = this.getPeopleId();
127
+ if (peopleId) {
128
+ this.sendEvent('user_page_visit');
129
+ } else {
130
+ this.sendEvent('visitor_page_visit');
131
+ }
122
132
  }
123
133
 
124
134
  /**
125
- * Handle an event and check for matching automations
135
+ * Send an event to the backend
126
136
  * @param {string} eventType - The type of event
127
137
  * @param {Object} eventData - Additional event data
128
138
  */
129
- handleEvent(eventType, eventData = {}) {
130
- this.automations.forEach((automation) => {
131
- const triggerNode = this.findTriggerNode(automation);
132
- if (triggerNode && triggerNode.data?.eventType === eventType) {
133
- this.evaluateAndExecute(automation, triggerNode, eventData);
139
+ async sendEvent(eventType, eventData = {}) {
140
+ if (!eventType) return;
141
+
142
+ try {
143
+ const response = await fetch(`${API}/ewidget/events`, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ 'apiKey': this.apiKey,
148
+ 'peopleId': this.getPeopleId() || '',
149
+ 'anonymousId': this.getAnonymousId() || ''
150
+ },
151
+ body: JSON.stringify({
152
+ eventType,
153
+ context: {
154
+ ...eventData,
155
+ pageUrl: window.location.href,
156
+ pagePath: window.location.pathname
157
+ },
158
+ firstSeenTimestamp: this.firstSeenTimestamp
159
+ })
160
+ });
161
+
162
+ if (!response.ok) {
163
+ console.warn('[Rbird] Failed to send event:', response.status);
164
+ } else {
165
+ console.log('[Rbird] Event sent:', eventType);
134
166
  }
135
- });
167
+ } catch (error) {
168
+ console.error('[Rbird] Error sending event:', error);
169
+ }
136
170
  }
137
171
 
138
172
  /**
139
- * Find the trigger node in an automation
140
- * @param {Object} automation - The automation object
141
- * @returns {Object|null} The trigger node or null
173
+ * Get the current people ID from session
142
174
  */
143
- findTriggerNode(automation) {
144
- return automation.nodes?.find((node) => node.type === 'trigger');
175
+ getPeopleId() {
176
+ return RbirdSessionManager.getInstance().getState()?.identify?.peopleId;
145
177
  }
146
178
 
147
179
  /**
148
- * Evaluate trigger conditions and execute the automation
149
- * @param {Object} automation - The automation object
150
- * @param {Object} triggerNode - The trigger node
151
- * @param {Object} context - The event context
180
+ * Get the anonymous ID from session
152
181
  */
153
- evaluateAndExecute(automation, triggerNode, context) {
154
- // Check if automation was already executed (for once-only automations)
155
- if (this.executedAutomations.has(automation.id)) {
156
- return;
157
- }
158
-
159
- // Evaluate trigger conditions
160
- if (!this.evaluateTriggerConditions(triggerNode, context)) {
161
- return;
162
- }
163
-
164
- // Mark as executed
165
- this.executedAutomations.add(automation.id);
166
-
167
- // Execute the workflow starting from the trigger node
168
- this.executeWorkflow(automation, triggerNode.id, context);
182
+ getAnonymousId() {
183
+ return RbirdSessionManager.getInstance().getState()?.identify?.anonymousId ||
184
+ localStorage.getItem('rbird_anonymous_id') ||
185
+ this.generateAnonymousId();
169
186
  }
170
187
 
171
188
  /**
172
- * Evaluate trigger conditions
173
- * @param {Object} triggerNode - The trigger node
174
- * @param {Object} context - The event context
175
- * @returns {boolean} Whether conditions are met
189
+ * Generate and store an anonymous ID
176
190
  */
177
- evaluateTriggerConditions(triggerNode, context) {
178
- const conditions = triggerNode.data?.conditions || [];
179
-
180
- for (const condition of conditions) {
181
- const value = this.getContextValue(condition.property, context);
182
-
183
- switch (condition.operator) {
184
- case 'less_than':
185
- if (!(parseFloat(value) < parseFloat(condition.value))) return false;
186
- break;
187
- case 'greater_than':
188
- if (!(parseFloat(value) > parseFloat(condition.value))) return false;
189
- break;
190
- case 'equals':
191
- if (value !== condition.value) return false;
192
- break;
193
- case 'contains':
194
- if (!String(value).includes(condition.value)) return false;
195
- break;
196
- }
197
- }
198
-
199
- return true;
191
+ generateAnonymousId() {
192
+ const id = 'anon_' + Math.random().toString(36).substr(2, 9);
193
+ localStorage.setItem('rbird_anonymous_id', id);
194
+ return id;
200
195
  }
201
196
 
202
197
  /**
203
- * Get a value from the context
204
- * @param {string} property - The property path
205
- * @param {Object} context - The context object
206
- * @returns {any} The value
198
+ * Start polling for pending commands.
199
+ * Polling is now a fallback mechanism - WebSocket is the primary delivery method.
200
+ * The interval is set to 60 seconds to catch any missed commands.
207
201
  */
208
- getContextValue(property, context) {
209
- if (property === 'time_since_first_seen' || property === 'timeSinceFirstSeen') {
210
- return context.timeSinceFirstSeen || (Date.now() - this.firstSeenTimestamp) / 1000;
202
+ startCommandPolling() {
203
+ if (this.pollingInterval) {
204
+ return; // Already polling
211
205
  }
212
206
 
213
- // Navigate nested properties like "user.language"
214
- const parts = property.split('.');
215
- let value = context;
216
- for (const part of parts) {
217
- value = value?.[part];
218
- }
219
- return value;
207
+ // Poll immediately once
208
+ this.pollCommands();
209
+
210
+ // Poll every 60 seconds as fallback (WebSocket handles instant delivery)
211
+ this.pollingInterval = setInterval(() => this.pollCommands(), 60000);
220
212
  }
221
213
 
222
214
  /**
223
- * Execute a workflow from a starting node
224
- * @param {Object} automation - The automation object
225
- * @param {string} startNodeId - The ID of the starting node
226
- * @param {Object} context - The execution context
215
+ * Stop command polling
227
216
  */
228
- executeWorkflow(automation, startNodeId, context) {
229
- // Find next nodes to execute
230
- const nextEdges = automation.edges?.filter((edge) => edge.source === startNodeId) || [];
231
-
232
- for (const edge of nextEdges) {
233
- const targetNode = automation.nodes?.find((node) => node.id === edge.target);
234
- if (!targetNode) continue;
235
-
236
- if (targetNode.type === 'condition') {
237
- // Evaluate condition and follow appropriate path
238
- const conditionResult = this.evaluateCondition(targetNode, context);
239
- const handleToFollow = conditionResult ? 'true' : 'false';
240
-
241
- // Find edges from this condition with matching handle
242
- const conditionEdges = automation.edges?.filter(
243
- (e) => e.source === targetNode.id && e.sourceHandle === handleToFollow
244
- ) || [];
245
-
246
- for (const condEdge of conditionEdges) {
247
- this.executeWorkflow(automation, condEdge.source, context);
248
- }
249
- } else if (targetNode.type === 'action') {
250
- // Execute the action
251
- this.executeAction(targetNode, context);
252
-
253
- // Continue to next nodes if any
254
- this.executeWorkflow(automation, targetNode.id, context);
255
- }
217
+ stopCommandPolling() {
218
+ if (this.pollingInterval) {
219
+ clearInterval(this.pollingInterval);
220
+ this.pollingInterval = null;
256
221
  }
257
222
  }
258
223
 
259
224
  /**
260
- * Evaluate a condition node
261
- * @param {Object} conditionNode - The condition node
262
- * @param {Object} context - The execution context
263
- * @returns {boolean} The result of the condition
225
+ * Poll for pending commands from the backend
264
226
  */
265
- evaluateCondition(conditionNode, context) {
266
- const { property, operator, value } = conditionNode.data || {};
267
-
268
- let actualValue;
269
-
270
- // Get the value to compare
271
- if (property?.startsWith('user.')) {
272
- const userProp = property.replace('user.', '');
273
- actualValue = RbirdSessionManager.getInstance().getUser()?.[userProp];
274
- } else if (property?.startsWith('page.')) {
275
- const pageProp = property.replace('page.', '');
276
- if (pageProp === 'url') {
277
- actualValue = window.location.href;
278
- } else if (pageProp === 'path') {
279
- actualValue = window.location.pathname;
227
+ async pollCommands() {
228
+ if (this.isPolling) return; // Prevent concurrent polls
229
+ this.isPolling = true;
230
+
231
+ try {
232
+ const response = await fetch(`${API}/ewidget/commands`, {
233
+ method: 'GET',
234
+ headers: {
235
+ 'Content-Type': 'application/json',
236
+ 'apiKey': this.apiKey,
237
+ 'peopleId': this.getPeopleId() || '',
238
+ 'anonymousId': this.getAnonymousId() || ''
239
+ }
240
+ });
241
+
242
+ if (!response.ok) {
243
+ return;
280
244
  }
281
- } else {
282
- actualValue = context[property];
283
- }
284
245
 
285
- switch (operator) {
286
- case 'equals':
287
- return actualValue === value;
288
- case 'not_equals':
289
- return actualValue !== value;
290
- case 'contains':
291
- return String(actualValue).includes(value);
292
- case 'starts_with':
293
- return String(actualValue).startsWith(value);
294
- default:
295
- return false;
246
+ const commands = await response.json();
247
+
248
+ // Execute each command
249
+ for (const cmd of commands) {
250
+ await this.executeCommand(cmd);
251
+ await this.markDelivered(cmd.id);
252
+ }
253
+ } catch (error) {
254
+ // Silently fail - polling will retry
255
+ } finally {
256
+ this.isPolling = false;
296
257
  }
297
258
  }
298
259
 
299
260
  /**
300
- * Execute an action node
301
- * @param {Object} actionNode - The action node
302
- * @param {Object} context - The execution context
261
+ * Execute a command from the backend
262
+ * @param {Object} cmd - The command to execute
303
263
  */
304
- executeAction(actionNode, context) {
305
- const { actionType, config } = actionNode.data || {};
264
+ async executeCommand(cmd) {
265
+ console.log('[Rbird] Executing command:', cmd.type);
306
266
 
307
- console.log('[Rbird] Executing action:', actionType, config);
308
-
309
- switch (actionType) {
310
- case 'send_chat':
311
- this.executeSendChat(config?.message, context);
312
- break;
267
+ switch (cmd.type) {
313
268
  case 'show_banner':
314
- this.executeShowBanner(config?.bannerId);
269
+ this.executeShowBanner(cmd.targetId);
315
270
  break;
316
271
  case 'show_form':
317
- this.executeShowForm(config?.formId);
272
+ this.executeShowForm(cmd.targetId);
318
273
  break;
319
274
  case 'show_survey':
320
- this.executeShowSurvey(config?.surveyId);
275
+ this.executeShowSurvey(cmd.targetId);
321
276
  break;
322
- case 'webhook':
323
- this.executeWebhook(config?.webhookUrl, config?.webhookMethod || 'POST', context);
277
+ case 'send_chat':
278
+ this.executeSendChat(cmd.message, cmd.config);
324
279
  break;
280
+ default:
281
+ console.warn('[Rbird] Unknown command type:', cmd.type);
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Mark a command as delivered
287
+ * @param {string} commandId - The command ID
288
+ */
289
+ async markDelivered(commandId) {
290
+ try {
291
+ await fetch(`${API}/ewidget/commands/${commandId}/delivered`, {
292
+ method: 'POST',
293
+ headers: {
294
+ 'Content-Type': 'application/json',
295
+ 'apiKey': this.apiKey
296
+ }
297
+ });
298
+ } catch (error) {
299
+ console.warn('[Rbird] Failed to mark command as delivered:', error);
325
300
  }
326
301
  }
327
302
 
328
303
  /**
329
- * Execute send_chat action
304
+ * Execute send_chat command
330
305
  */
331
- executeSendChat(message, context) {
306
+ executeSendChat(message, config) {
332
307
  if (!message) return;
333
308
 
334
- // Replace placeholders in message
335
- const processedMessage = this.processMessage(message, context);
309
+ console.log('[Rbird] Chat message triggered:', message);
336
310
 
337
- // Dispatch event for the chat widget to handle
338
- document.dispatchEvent(new CustomEvent('rbird:automation_chat', {
339
- detail: { message: processedMessage }
340
- }));
311
+ // Open the widget and send the message
312
+ const widget = RbirdWebsiteWidget.getInstance();
313
+ widget.openWebsiteWidget();
341
314
 
342
- console.log('[Rbird] Chat message triggered:', processedMessage);
315
+ // Send message to iframe to display automation message
316
+ setTimeout(() => {
317
+ if (widget.iframe) {
318
+ widget.iframe.contentWindow?.postMessage({
319
+ type: 'automationMessage',
320
+ message: message
321
+ }, '*');
322
+ }
323
+ }, 500); // Small delay to ensure iframe is ready
343
324
  }
344
325
 
345
326
  /**
346
- * Execute show_banner action
327
+ * Execute show_banner command
347
328
  */
348
329
  executeShowBanner(bannerId) {
349
330
  if (!bannerId) return;
@@ -351,7 +332,7 @@ export class RbirdAutomationManager {
351
332
  }
352
333
 
353
334
  /**
354
- * Execute show_form action
335
+ * Execute show_form command
355
336
  */
356
337
  executeShowForm(formId) {
357
338
  if (!formId) return;
@@ -359,48 +340,13 @@ export class RbirdAutomationManager {
359
340
  }
360
341
 
361
342
  /**
362
- * Execute show_survey action
343
+ * Execute show_survey command
363
344
  */
364
345
  executeShowSurvey(surveyId) {
365
346
  if (!surveyId) return;
366
347
  RbirdSurveyManager.getInstance().showSurvey(surveyId, {});
367
348
  }
368
349
 
369
- /**
370
- * Execute webhook action
371
- */
372
- async executeWebhook(url, method, context) {
373
- if (!url) return;
374
-
375
- try {
376
- const options = {
377
- method: method,
378
- headers: {
379
- 'Content-Type': 'application/json',
380
- },
381
- };
382
-
383
- if (method === 'POST') {
384
- options.body = JSON.stringify(context);
385
- }
386
-
387
- await fetch(url, options);
388
- console.log('[Rbird] Webhook executed:', url);
389
- } catch (error) {
390
- console.error('[Rbird] Webhook error:', error);
391
- }
392
- }
393
-
394
- /**
395
- * Process message placeholders
396
- */
397
- processMessage(message, context) {
398
- return message
399
- .replace(/\{\{user\.name\}\}/g, RbirdSessionManager.getInstance().getUser()?.name || '')
400
- .replace(/\{\{user\.email\}\}/g, RbirdSessionManager.getInstance().getUser()?.email || '')
401
- .replace(/\{\{page\.url\}\}/g, window.location.href);
402
- }
403
-
404
350
  /**
405
351
  * Track a custom event
406
352
  * @param {string} eventName - The name of the event
@@ -411,6 +357,13 @@ export class RbirdAutomationManager {
411
357
  detail: { eventName, data }
412
358
  }));
413
359
  }
360
+
361
+ /**
362
+ * Cleanup when manager is destroyed
363
+ */
364
+ destroy() {
365
+ this.stopCommandPolling();
366
+ }
414
367
  }
415
368
 
416
369
  export default RbirdAutomationManager;