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.
- package/build/index.js +1 -1
- package/package.json +3 -2
- package/published/1.0.87/index.js +1 -1
- package/published/1.0.88/index.js +1 -0
- package/published/1.0.89/index.js +1 -0
- package/published/latest/index.js +1 -1
- package/src/RbirdAutomationManager.js +416 -0
- package/src/RbirdFormManager.js +7 -2
- package/src/RbirdSurveyManager.js +720 -0
- package/src/RbirdWebsiteWidget.js +89 -8
- package/src/index.js +42 -0
|
@@ -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;
|
package/src/RbirdFormManager.js
CHANGED
|
@@ -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:
|
|
357
|
-
userName:
|
|
361
|
+
userEmail: userEmail,
|
|
362
|
+
userName: userName
|
|
358
363
|
}));
|
|
359
364
|
}
|
|
360
365
|
|