releasebird-javascript-sdk 1.0.87 → 1.0.89

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.
@@ -0,0 +1,416 @@
1
+ import RbirdSessionManager from "./RbirdSessionManager";
2
+ import { RbirdBannerManager } from "./RbirdBannerManager";
3
+ import { RbirdFormManager } from "./RbirdFormManager";
4
+ import { RbirdSurveyManager } from "./RbirdSurveyManager";
5
+
6
+ /**
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.
10
+ */
11
+ export class RbirdAutomationManager {
12
+ static instance = null;
13
+ automations = [];
14
+ apiKey = null;
15
+ firstSeenTimestamp = null;
16
+ executedAutomations = new Set(); // Track which automations have been executed
17
+
18
+ static getInstance() {
19
+ if (!this.instance) {
20
+ this.instance = new RbirdAutomationManager();
21
+ }
22
+ return this.instance;
23
+ }
24
+
25
+ /**
26
+ * Initialize the automation manager
27
+ * @param {string} apiKey - The API key for the project
28
+ */
29
+ async init(apiKey) {
30
+ this.apiKey = apiKey;
31
+ this.firstSeenTimestamp = this.getFirstSeenTimestamp();
32
+
33
+ // Load automations from backend
34
+ await this.loadAutomations();
35
+
36
+ // Register event handlers
37
+ this.registerEventHandlers();
38
+
39
+ // Check for first_seen triggers immediately
40
+ this.checkFirstSeenTriggers();
41
+ }
42
+
43
+ /**
44
+ * Get or set the first_seen timestamp
45
+ */
46
+ getFirstSeenTimestamp() {
47
+ const stored = localStorage.getItem('rbird_first_seen');
48
+ if (stored) {
49
+ return parseInt(stored, 10);
50
+ }
51
+ const now = Date.now();
52
+ localStorage.setItem('rbird_first_seen', now.toString());
53
+ return now;
54
+ }
55
+
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
+ /**
83
+ * Register event handlers for automation triggers
84
+ */
85
+ registerEventHandlers() {
86
+ // Listen for page visits
87
+ window.addEventListener('popstate', () => this.handleEvent('page_visit'));
88
+
89
+ // Listen for identify events
90
+ document.addEventListener('rbird:identify', (e) => {
91
+ this.handleEvent('user_identified', e.detail);
92
+ });
93
+
94
+ // Listen for form submissions
95
+ document.addEventListener('rbird:form_submitted', (e) => {
96
+ this.handleEvent('form_submitted', e.detail);
97
+ });
98
+
99
+ // Listen for survey completions
100
+ document.addEventListener('rbird:survey_completed', (e) => {
101
+ this.handleEvent('survey_completed', e.detail);
102
+ });
103
+
104
+ // Listen for custom events
105
+ document.addEventListener('rbird:track', (e) => {
106
+ this.handleEvent(e.detail?.eventName, e.detail?.data);
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Check for first_seen triggers on initialization
112
+ */
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
+ });
122
+ }
123
+
124
+ /**
125
+ * Handle an event and check for matching automations
126
+ * @param {string} eventType - The type of event
127
+ * @param {Object} eventData - Additional event data
128
+ */
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);
134
+ }
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Find the trigger node in an automation
140
+ * @param {Object} automation - The automation object
141
+ * @returns {Object|null} The trigger node or null
142
+ */
143
+ findTriggerNode(automation) {
144
+ return automation.nodes?.find((node) => node.type === 'trigger');
145
+ }
146
+
147
+ /**
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
152
+ */
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);
169
+ }
170
+
171
+ /**
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
176
+ */
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;
200
+ }
201
+
202
+ /**
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
207
+ */
208
+ getContextValue(property, context) {
209
+ if (property === 'time_since_first_seen' || property === 'timeSinceFirstSeen') {
210
+ return context.timeSinceFirstSeen || (Date.now() - this.firstSeenTimestamp) / 1000;
211
+ }
212
+
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;
220
+ }
221
+
222
+ /**
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
227
+ */
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
+ }
256
+ }
257
+ }
258
+
259
+ /**
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
264
+ */
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;
280
+ }
281
+ } else {
282
+ actualValue = context[property];
283
+ }
284
+
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;
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Execute an action node
301
+ * @param {Object} actionNode - The action node
302
+ * @param {Object} context - The execution context
303
+ */
304
+ executeAction(actionNode, context) {
305
+ const { actionType, config } = actionNode.data || {};
306
+
307
+ console.log('[Rbird] Executing action:', actionType, config);
308
+
309
+ switch (actionType) {
310
+ case 'send_chat':
311
+ this.executeSendChat(config?.message, context);
312
+ break;
313
+ case 'show_banner':
314
+ this.executeShowBanner(config?.bannerId);
315
+ break;
316
+ case 'show_form':
317
+ this.executeShowForm(config?.formId);
318
+ break;
319
+ case 'show_survey':
320
+ this.executeShowSurvey(config?.surveyId);
321
+ break;
322
+ case 'webhook':
323
+ this.executeWebhook(config?.webhookUrl, config?.webhookMethod || 'POST', context);
324
+ break;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Execute send_chat action
330
+ */
331
+ executeSendChat(message, context) {
332
+ if (!message) return;
333
+
334
+ // Replace placeholders in message
335
+ const processedMessage = this.processMessage(message, context);
336
+
337
+ // Dispatch event for the chat widget to handle
338
+ document.dispatchEvent(new CustomEvent('rbird:automation_chat', {
339
+ detail: { message: processedMessage }
340
+ }));
341
+
342
+ console.log('[Rbird] Chat message triggered:', processedMessage);
343
+ }
344
+
345
+ /**
346
+ * Execute show_banner action
347
+ */
348
+ executeShowBanner(bannerId) {
349
+ if (!bannerId) return;
350
+ RbirdBannerManager.getInstance().showBanner(bannerId);
351
+ }
352
+
353
+ /**
354
+ * Execute show_form action
355
+ */
356
+ executeShowForm(formId) {
357
+ if (!formId) return;
358
+ RbirdFormManager.getInstance().showForm(formId, {});
359
+ }
360
+
361
+ /**
362
+ * Execute show_survey action
363
+ */
364
+ executeShowSurvey(surveyId) {
365
+ if (!surveyId) return;
366
+ RbirdSurveyManager.getInstance().showSurvey(surveyId, {});
367
+ }
368
+
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
+ /**
405
+ * Track a custom event
406
+ * @param {string} eventName - The name of the event
407
+ * @param {Object} data - Additional event data
408
+ */
409
+ track(eventName, data = {}) {
410
+ document.dispatchEvent(new CustomEvent('rbird:track', {
411
+ detail: { eventName, data }
412
+ }));
413
+ }
414
+ }
415
+
416
+ export default RbirdAutomationManager;
@@ -351,10 +351,15 @@ export class RbirdFormManager {
351
351
  }
352
352
  };
353
353
 
354
+ // Determine user info - use identified user data or fallback to visitor with anonymous ID
355
+ const userEmail = state?.identify?.properties?.email || state?.identify?.email || null;
356
+ const userName = state?.identify?.properties?.name || state?.identify?.name ||
357
+ (sessionManager.anonymousIdentifier ? `Visitor (${sessionManager.anonymousIdentifier.substring(0, 8)})` : 'Visitor');
358
+
354
359
  http.send(JSON.stringify({
355
360
  data: data,
356
- userEmail: state?.identify?.email || null,
357
- userName: state?.identify?.name || null
361
+ userEmail: userEmail,
362
+ userName: userName
358
363
  }));
359
364
  }
360
365