diff --git a/src/mixins/telemetry/README.md b/src/mixins/telemetry/README.md
new file mode 100644
index 00000000..45d1ef07
--- /dev/null
+++ b/src/mixins/telemetry/README.md
@@ -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``
+ }
+}
+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``
+ }
+}
+customElement.define('my-component', MyComponent);
+```
diff --git a/src/mixins/telemetry/TelemetryEvent.js b/src/mixins/telemetry/TelemetryEvent.js
new file mode 100644
index 00000000..59ae5d02
--- /dev/null
+++ b/src/mixins/telemetry/TelemetryEvent.js
@@ -0,0 +1,14 @@
+
+export class TelemetryEvent {
+ static dispatch(elm, action, property, value) {
+ elm.dispatchEvent(
+ new CustomEvent('d2l-telemetry-event', {
+ detail: {
+ action,
+ property,
+ value
+ }
+ })
+ )
+ }
+}
diff --git a/src/mixins/telemetry/TelemetryMixin.js b/src/mixins/telemetry/TelemetryMixin.js
new file mode 100644
index 00000000..26610681
--- /dev/null
+++ b/src/mixins/telemetry/TelemetryMixin.js
@@ -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);
+ }
+ }
+}