Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Telemetry Mixin #27

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
73 changes: 73 additions & 0 deletions src/mixins/telemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
This builds off of https://github.com/Brightspace/discovery-fra/blob/master/src/mixins/telemetry-mixin.js and uses https://github.com/Brightspace/d2l-telemetry-browser-client

# Telemetry Mixin

Developers can use the telemetry mixin by defining their actions, properties and sourceId in an object and passing that object to the telemetry mixin. This mixin will add a custom event listener to the element which will listen for events with the 'd2l-telementry-event' type. The TelemetryEvent is a helper object to dispatch the event which will bubble up to the Mixin and get fired.

## Design Choices

A bubbling event design is used to allow developers to define multiple telemetry mixins for SPA's which may have different source id's per page. For example an application where each tool is a different page may want to track different events but keep a top level telemetry mixin for navigation events.

## Example

#### telemetryConfig.js
```js
// symbols are used to verify that events use the same actions and properties defined in the options.
const telemetryOptions = {
sourceId: "insightsAdoption",
actions: {
filtered: Symbol('Filtered'),
focused: Symbol('Focused'),
zoomed: Symbol('Zoomed'),
drilled: Symbol('Drilled')
},
properties: {
numRoles: Symbol('NumRoles'),
numTools: Symbol('NumTools'),
numOrgs: Symbol('NumOrgs'),
chart: Symbol('Chart')
},
// potential optional configurations
debounce: 5000, // milliseconds
fireOnClose: false, // onUnload event dispatch instead of sending request per event
middleware: telemetryEvent => {}, // modify the final event object before it is sent to the telemetry service.
}
```
### app.js
top level component, could be a SPA router or the application container
```js
import {telemetryConfig} from '../telemetryConfig.js'
import {TelemetryMixin} from '@d2l/telemetry'

class AdoptionDashboard extends TelemetryMixin(telemetryOptions)(LitElement) {
render() {
return html`<my-component></my-component>`
}
}
customElement.define('d2l-adoption-dashboard', AdoptionDashboard)
```

#### myComponent.js
```js

import {TelemetryEvent} from '@d2l/telemetry'
import {telemetryOptions} from '../telemetryConfig.js'

class MyComponent extends LitElement() {

handleClick(e) {
// telemetry mixin will handle verifying that these actions match the symbols defined
// during initialization
TelemetryEvent.dispatch(this, {
action: telemetryOptions.actions.filtered,
property: telemetryOptions.properties.numRoles,
value: e.target.value
}
}

render() {
return html`<button @click=${handleClick}>Fire Event</button>`
}
}
customElement.define('my-component', MyComponent);
```
14 changes: 14 additions & 0 deletions src/mixins/telemetry/TelemetryEvent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

export class TelemetryEvent {
static dispatch(elm, action, property, value) {
elm.dispatchEvent(
new CustomEvent('d2l-telemetry-event', {
detail: {
action,
property,
value
}
})
)
}
}
107 changes: 107 additions & 0 deletions src/mixins/telemetry/TelemetryMixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Generic of https://github.com/Brightspace/discovery-fra/blob/master/src/mixins/telemetry-mixin.js

export const TelemetryMixin = options => superClass => class TelemetryMixinClass extends superClass {
get actions() {
return options.actions;
}

get properties() {
return options.properties;
}

get fireOnClose() {
return options.fireOnClose;
}

get debounce() {
return options.debounce;
}

constructor() {
super();
const requiredKeys = [
"sourceId",
"actions",
"properties",
"endpoint"
];

if(!requiredKeys.every(key => Object.keys(options).includes(key))){
const foundMissing = requiredKeys.find(key => !Object.keys(options).includes(key));
throw new Error(`Telemetry options must have all required keys. Missing ${foundMissing}`);
}
if(!options.actions.every(action => typeof action === 'symbol') ) {
throw new Error(`Telemetry actions must be symbols`);
}
if(!options.properties.every(prop => typeof prop === 'symbol') ) {
throw new Error(`Telemetry properties must be symbols`);
}

this.client = new d2lTelemetryBrowserClient.Client({
endpoint: options.endpoint
});

if(this.fireOnClose === true) {
this.eventQueue = [];
}
}

_handleTelemetryEvent(e) {
const {action, property, value} = e.detail;
if (action === undefined || property === undefined) {
throw new Error('Telemetry events require an action and a property')
}
if(this.actions.includes(action) && this.properties.includes(property)) {

const eventBody = d2lTelemetryBrowserClient.EventBody()
.setAction(action)
.setObject(encodeURIComponent(value), property, window.location.href, value);

const event = new d2lTelemetryBrowserClient.TelemetryEvent()
.setDate(new Date())
.setType("TelemetryEvent")
.setSourceId(options.sourceId)
.setBody(eventBody);

if(options.middleware) {
options.middleware(event);
}

if(!this.fireOnClose || !this.debounce || this.debounce === 0) {
this.client.logUserEvent(event);
} else {
this._storeEventAndDebounce(event)
}

} else {
throw new Error("Telemetry event actions and properties must be from the defined symbol list");
}
}

_storeEventAndDebounce(event) {
this.eventQueue.push(event);
if(this.debounce && this.debounce !== 0) {
if(this.debounceTimeout) clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(
() => this.eventQueue.forEach(storedEvent => this.client.logUserEvent(storedEvent)),
this.debounce
);
}
}

_handleVisibilityChange(e) {
if(e.visibilityState === "hidden"){
this.eventQueue.forEach(event =>
this.client.logUserEvent(event)
);
}
}

connectedCallback() {
super.connectedCallback();
this.addEventListener('d2l-telemetry-event', this._handleTelemetryEvent);
if(options.fireOnClose || (options.debounce && options.debounce !== 0)) {
this.addEventListener('visibilitychange', this._handleVisibilityChange);
}
}
}