Skip to content
This repository has been archived by the owner on Apr 16, 2024. It is now read-only.

[RUM-24816][FEATURE] added experimental triggering mechanism. added a… #85

Open
wants to merge 4 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions examples/Vanilla/OnLoad/onload.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,22 @@ function rciMainAction(tenancyId, rciSdk) {

// Step 2: Capture your default collectors
let defaults = rciSdk.collector.defaultCollectors;

/**
* @type {Config}
*/
const config = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robertdumitrescu CONSISTENCY - I'm all for composition, leading to maximum flexibility. However, if we're going through the trouble of configuring the collectors, I'd like to see all the concatenation and registering to be handled. For example, if something like the following is provided:

const config = {
   transport: new rciSdk.Transport(targetUrl),
   onload: {
     defaults: true,
     enabled: true,
     custom: [...]
   },
   actions: {
     defaults: true,
     enabled: true,
     custom: [...]
   }
}
rciSdk.ConfigurationService.run(config);

That should register all triggers and collectors with the relevant producer. Job done!

actions: true
actions: true,
actionsBatching: false,
events: [
{
selector: 'window', eventName: 'load', eventCategory: 'Document', scope: 'state'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robertdumitrescu CONSISTENCY - The naming conventions here are becoming a little unclear - particularly as the names crossover from Web APIs to RCI's Data API. I would suggest referencing terms from the Moz documentation and the Data API directly, for example:

{
        target: 'window', type: 'load', rciEventAction: 'full_page_load', rciEventType: 'state'
}

I would also suggest that we plumb the rciEventAction value all the way through to eventAction in the Data API.

},
{
selector: '*', eventName: 'mousedown', eventCategory: 'Mouse', scope: 'action'
}
]

};

// Step 2.1 Prepare the collectors collection
Expand All @@ -15,8 +29,8 @@ function rciMainAction(tenancyId, rciSdk) {
// Step 3: Build a new Producer with transport and collector
const producer = new rciSdk.Producer(transport, defaults);

// Step 3.2: Register the action triggers
rciSdk.TriggerHelper.registerActionTriggers(producer, config);
// Step 3.2: Register the triggers
rciSdk.TriggerHelper.registerTriggers(producer, config);

// Step 4: Trigger Event
window.addEventListener('load', async () => {
Expand Down
30 changes: 30 additions & 0 deletions src/collector/ActionBatchingCollector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

const localStorageActionsKey = 'eggplantRciActions';
export default class ActionBatchingCollector {

constructor(localStorage) {
this.localStorage = localStorage;
}

/**
* @param {Event} event
* @param {Context} context
*/
async prepare (event, context) {
/** Pull out the actions from local storage */
const actions = this.localStorage.getItem(localStorageActionsKey);

/** Append the new action */
actions.push(event.eventInfo5[0]);
if (context.scope === 'state') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robertdumitrescu MAINTAINABILITY - I'd rather we use ENUMs than lose strings. We have some set up here: https://github.com/TestPlant/real-user-data-sdk-js/blob/development/src/core/constants.js#L1


/** Stringify actions and attached it to the state beacon */
event.eventInfo5 = JSON.stringify(actions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robertdumitrescu INFO - We need to be careful about string length as anything above 1024 chars will be rejected. This is much lower than I had originally thought... https://docs.real-user-data.eggplant.cloud/open-api/index.html


} else if (context.scope === 'action') {
this.localStorage.setItem(localStorageActionsKey, actions);
}
return event;
}

}
12 changes: 12 additions & 0 deletions src/collector/ActionStringifyCollector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default class ActionStringifyCollector {

/**
* @param {Event} event
*/
async prepare (event) {

event.eventInfo5 = JSON.stringify(event.eventInfo5);
return event;
}

}
13 changes: 13 additions & 0 deletions src/collector/InitializeActionCollector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default class InitializeActionCollector {


/**
* @param {Event} event
* @param {Context} context
*/
async prepare (event) {
event.eventInfo5 = [{}];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robertdumitrescu KISS - Is the empty object required here..?

return event;
}

}
22 changes: 19 additions & 3 deletions src/core/ConfigurationService.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
import actionCollectors from '../collector/actionCollectors';
import InitializeActionCollector from '../collector/InitializeActionCollector';
import ActionBatchingCollector from '../collector/ActionBatchingCollector';
import ActionStringifyCollector from '../collector/ActionStringifyCollector';

export default class ConfigurationService {

/**
*
* @param {Collector[]} collectors
* @param {Object} config
* @param {Config} config
*/
static prepareCollectors (collectors, config) {

collectors = Array.isArray(collectors) ? [].concat(collectors) : [];

if (config.actions === true) {
collectors = collectors.concat(actionCollectors);
}
/**
* Make sure that the following collectors are kept in this order to avoid weird behaviours
*/

collectors.push(new InitializeActionCollector());

collectors = collectors.concat(actionCollectors);

if (config.actionsBatching === true) {
collectors.push(new ActionBatchingCollector());
} else {
collectors.push(new ActionStringifyCollector());
}

}

return collectors;
}
Expand Down
4 changes: 4 additions & 0 deletions src/core/Producer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export default class Producer {
* @param {Context} context
*/
async prepareData(collectors, context) {
if (!Array.isArray(collectors) || collectors.length === 0) {
collectors = this.collectors;
}

let event = {};

for (let i = 0; i < collectors.length; i += 1) {
Expand Down
62 changes: 37 additions & 25 deletions src/core/Trigger.helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,38 +118,50 @@ export default class TriggerHelper {
});
}

static async eventHandler (event, context, producer) {
try {
context.elm = event.target || event.srcElement;
console.log(`Sending event on ${context.eventName}`);
await producer.prepareData([], context);
} catch (e) {
console.log(e);
}
}


/**
* If actions are set on true, registers action triggers
*/
static async registerActionTriggers (producer, config) {
*
* @param {*} producer
* @param {Config} config
*/
static async registerTriggers (producer, config) {

if (config.actions === true) {
// Register all the relevant event handlers


for (let i = 0; i < config.events.length; i++) {
if (config.events[i].scope === 'action' && config.actions === false) {
continue;
}
/**
* @type {Context}
*/
let context = {};

document.querySelector('*').addEventListener('click', async (event) => {
try {
context.elm = event.target || event.srcElement;
console.log('Sending event on click');
await producer.collect(context);
} catch (e) {
console.log(e);
}
});

window.addEventListener('scroll', async (event) => {
try {
console.log('Sending event on scroll');
await producer.collect(context);
} catch (e) {
console.log(e);
}
});
context = {scope: config.events[i].scope, eventName: config.events[i].eventName};
let node;


/** Normalize node */
if (!(typeof config.events[i].selector === 'string' || config.events[i].selector instanceof String) || config.events[i].selector.length === 0) {
node = document.querySelector('*');
} else if (config.events[i].selector === 'window') {
node = window;
} else if (config.events[i].selector === 'document') {
node = document;
} else {
node = document.querySelector(config.events[i].selector);
}

node.addEventListener(config.events[i].eventName, (event) => TriggerHelper.eventHandler(event, context, producer));
}

}
}
41 changes: 41 additions & 0 deletions src/core/action.Definition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @typedef {Object} Action
* @link https://developer.mozilla.org/en-US/docs/Web/Events
* @description Considerdations: In case of clipboard events, for instance when somebody pastes, all the actionStart properties may be empty/null
* @property {String|null} actionStartEventCategory - Enum: "Clipboard", "Focus", "Drag and Drop", "Touch", "Keyboard", "Mouse". More to come, this is only an initial list that I feel we should support
* @property {String} actionEndEventCategory - Enum: "clipboard", "Focus", "Drag and Drop", "Touch", "Keyboard", "Mouse". More to come, this is only an initial list that I feel we should support
* @property {String|null} actionStartEventName - The type of the event that started the action. Ex: mousedown
* @property {String} actionEndEventName - The type of the event that ended the action. Ex: mouseup
* @property {Number|null} actionStartEventTimestamp - When the action started in miliseconds
* @property {Number} actionEndEventTimestamp - When the action ended in miliseconds
* @property {String|null} actionStartElementCategory - Enum: "document", "window", "StaticElement", "FormElement"
* @property {String} actionEndElementCategory - Enum: "document", "window", "StaticElement", "FormElement"
* @property {Element|null} actionStartElement - In case of clicks/swipes the element that was under the pointer/finger when the action started. In case of scroll the "document". In case of pan in/pan out, the element that was zoomed in/out. In case of device tilt, the "document"
* @property {Element} actionEndElement - In case of clicks/swipes the element that was under the pointer/finger when the action ended. In case of scroll the "document". In case of pan in/pan out, the element that was zoomed in/out. In case of device tilt, the "document"
* @property {String|null} actionStartElementXPath - The XPath of the element
* @property {String} actionEndElementXPath - The XPath of the element
* @property {String|null} actionStartElementCSSPath - The CSS path of the element
* @property {String} actionEndElementCSSPath - The CSS path of the element
* @property {String|null} actionStartElementViewPortCoords - The coordinates of the element in px relative to the top left corner of the screen separated by comma on horizontal and vertical axis
* @property {Number|null} actionStartElementWidth - The width in px of the element
* @property {Number|null} actionStartElementHeight - The height in px of the element
* @property {Boolean|null} actionStartElementHidden - Enum: null if not applicable, true if is hidden, false if is visible
* @property {Boolean|null} actionStartElementDisabled - Enum: null if not applicable, true if is disabled, false if is enabled
* @property {String} actionEndElementViewPortCoords - The coordinates of the element in px relative to the top left corner of the screen separated by comma on horizontal and vertical axis
* @property {Number} actionEndElementWidth - The width in px of the element
* @property {Number} actionEndElementHeight - The height in px of the element
* @property {Boolean} actionEndElementHidden - Enum: null if not applicable, true if is hidden, false if is visible
* @property {Boolean} actionEndElementDisabled - Enum: null if not applicable, true if is disabled, false if is enabled
* @property {String|null} actionStartElementImage - Cropped image of the element for OCR purposes
* @property {String} actionEndElementImage - Cropped image of the element for OCR purposes
* @property {ActionEventLocation[]} actionEventLocations - An array of objects with all the locations of the pointer, finger. This can be used to calculate heatmaps, rage clicks, rage abandonment, user anxiety and other various metrics for abandonment. Probably this can be a phase 2.
* */


/**
* PHASE 2
* @typedef {Object} ActionEventLocation
* @property {Number} hCoords - Horizontal coordinates in px relative to top left corner of the viewport
* @property {Number} vCoords - Vertical coordinates in px relative to top left corner of the viewport
* @property {Number} timestamp - Timestamp when the location was registered
* */
15 changes: 15 additions & 0 deletions src/core/config.Definition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @typedef {Object} Config
* @description Configuration object that composes the capability of the SDK
* @property {Boolean} actions - If this is on true, action beacons will be sent to the cloud (address defined in transport). If on false, only on load events (representing states) will be sent.
* @property {Boolean} actionsBatching - If this is on true, the actions beacons will be batched and sent alongside states. Otherwise will be sent as separate events.
* @property {ListenerDefinition[]} events - The events that will trigger beacons
* */

/**
* @typedef {Object} ListenerDefinition
* @property {String} selector - The selector on which the event will be listened for
* @property {String} eventName - The name of the event
* @property {String} eventCategory - The name of the event
* @property {String} scope - Either "state", either "action". Useful to identify if the actions array should be pulled from local storage when batching is adctivated and needs to be sent with qa state.
* */
2 changes: 2 additions & 0 deletions src/core/context.Definition.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/**
* @typedef {Object} Context
* @property {String} eventName
* @property {String} scope - Either "state", either "action". Useful to identify if the actions array should be pulled from local storage when batching is adctivated and needs to be sent with qa state.
* @property {HTMLElement} elm - The DOM element or HTML element (no matter from the context is coming from)
* */
2 changes: 1 addition & 1 deletion src/core/event.Definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
* @property {String} eventInfo2
* @property {String} eventInfo3
* @property {String} eventInfo4
* @property {String} eventInfo5
* @property {String|Action} eventInfo5
* @property {String} eventSource
* @property {String} eventCategory
* @property {String} goalType
Expand Down