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

Experimentation Reporting on RUM Explorer #622

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
178 changes: 178 additions & 0 deletions tools/oversight/charts/experimentsankey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// eslint-disable-next-line import/no-unresolved
import { SankeyController, Flow } from 'chartjs-chart-sankey';
// eslint-disable-next-line import/no-unresolved
import { Chart, registerables } from 'chartjs';
import SankeyChart from './sankey.js';
import { cssVariable, parseConversionSpec } from '../utils.js';

Chart.register(SankeyController, Flow, ...registerables);

const stages = [
{
label: 'contenttype',
/*
* 1. What kind of content was consumed
* In this case, it is just "experiment"
*/
experiment: {
label: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'experiment')
.map((e) => `experiment:${e.source}`)
.pop(),
color: cssVariable('--spectrum-red-800'),
detect: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'experiment')
.length > 0,
next: ['variant:*'],
},
},
{
label: 'variant',
/*
* 2. What variant is selected
* - variant
*/
variant: {
color: cssVariable('--spectrum-fuchsia-300'),
label: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'experiment')
.map((e) => `variant:${e.source} ${e.target}`)
.pop(),
detect: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'experiment')
.length > 0,
next: ['click', 'convert', 'formsubmit', 'nointeraction'],
},
},
{
label: 'interaction',
/*
* 3. What kind of interaction happened
* - convert
* - click
* - formsubmit
* - none
*/
convert: {
color: cssVariable('--spectrum-fuchsia-300'),
label: 'Conversion',
detect: (bundle, dataChunks) => {
const conversionSpec = parseConversionSpec();
if (Object.keys(conversionSpec).length === 0) return false;
if (Object.keys(conversionSpec).length === 1 && conversionSpec.checkpoint && conversionSpec.checkpoint[0] === 'click') return false;
return dataChunks.hasConversion(bundle, conversionSpec, 'every');
},
next: [],
},
click: {
color: cssVariable('--spectrum-green-900'),
label: 'Click',
detect: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'click')
.length > 0,
next: ['intclick', 'extclick', 'media'],
},
formsubmit: {
color: cssVariable('--spectrum-seafoam-900'),
label: 'Form Submit',
detect: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'formsubmit')
.length > 0,
},
nointeraction: {
label: 'No Interaction',
color: cssVariable('--spectrum-gray-100'),
detect: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'click'
|| e.checkpoint === 'formsubmit')
.length === 0,
},
},
{
label: 'clicktarget',
/*
* 4. What's the type of click target
* - external
* - internal
* - media
*/
media: {
color: cssVariable('--spectrum-yellow-1000'),
label: 'Media Click',
next: [],
detect: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'click')
.filter((e) => e.target && e.target.indexOf('media_') > -1)
.length > 0,
},
extclick: {
label: 'External Click',
color: cssVariable('--spectrum-purple-1000'),
next: ['external:*'],
detect: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'click')
.filter((e) => e.target && e.target.startsWith('http'))
.filter((e) => new URL(e.target).hostname !== new URL(bundle.url).hostname)
.length > 0,
},

intclick: {
label: 'Internal Click',
color: cssVariable('--spectrum-green-1000'),
next: ['internal:*'],
detect: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'click')
.filter((e) => !!e.target)
.filter((e) => new URL(e.target).hostname === new URL(bundle.url).hostname)
.length > 0,
},
},
{
label: 'exit',
/*
* 5. What's the click target, specifically
*/
internal: {
color: cssVariable('--spectrum-green-1100'),
next: [],
label: (bundle) => bundle.events.filter((e) => e.checkpoint === 'click')
.filter((e) => !!e.target)
.map((e) => new URL(e.target))
.map((u) => u.pathname)
.map((p) => `internal:${p}`)
.pop(),
detect: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'click')
.filter((e) => !!e.target)
.filter((e) => e.target.indexOf('media_') === -1)
.filter((e) => new URL(e.target).hostname === new URL(bundle.url).hostname)
.length > 0,
},
external: {
color: cssVariable('--spectrum-purple-1100'),
next: [],
label: (bundle) => bundle.events.filter((e) => e.checkpoint === 'click')
.filter((e) => !!e.target)
.map((e) => new URL(e.target))
.map((u) => u.hostname)
.map((h) => `external:${h}`)
.pop(),
detect: (bundle) => bundle.events
.filter((e) => e.checkpoint === 'click')
.filter((e) => e.target)
.filter((e) => e.target.indexOf('media_') === -1)
// only external links for now
.filter((e) => new URL(e.target).hostname !== new URL(bundle.url).hostname)
.length > 0,
},
},
];
const allStages = stages.reduce((acc, stage) => ({ ...acc, ...stage }), {});

export default class ExperimentSankeyChart extends SankeyChart {
constructor(dataChunks, elems) {
super(dataChunks, elems);
this.stages = stages;
this.allStages = allStages;
}
}
28 changes: 17 additions & 11 deletions tools/oversight/charts/sankey.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,12 @@ const stages = [
const allStages = stages.reduce((acc, stage) => ({ ...acc, ...stage }), {});

export default class SankeyChart extends AbstractChart {
constructor(dataChunks, elems) {
super(dataChunks, elems);
this.stages = stages;
this.allStages = allStages;
}

async draw() {
const params = new URL(window.location.href).searchParams;

Expand All @@ -411,7 +417,7 @@ export default class SankeyChart extends AbstractChart {
.reduce(reclassifyEnter, []);
const detectedFlows = [];
// go through each stage
stages.forEach((stage) => {
this.stages.forEach((stage) => {
let cont = false;
// go through each flow step in the stage
Object.entries(stage)
Expand Down Expand Up @@ -455,11 +461,11 @@ export default class SankeyChart extends AbstractChart {
const first = pair[0].split(':')[0];
const second = pair[1].split(':')[0];

if (allStages[first] && Array.isArray(allStages[first].next)) {
if (allStages[first].next.includes(second)) {
if (this.allStages[first] && Array.isArray(this.allStages[first].next)) {
if (this.allStages[first].next.includes(second)) {
// console.log('explicit allow', pair[0], '->', pair[1]);
return true;
} if (allStages[first].next
} if (this.allStages[first].next
.filter((n) => n.endsWith(':*'))
.map((n) => n.slice(0, -2))
.includes(second)) {
Expand Down Expand Up @@ -487,9 +493,9 @@ export default class SankeyChart extends AbstractChart {
.flat()
.reduce((acc, key) => {
const [prefix, suffix] = key.split(':');
if (allStages[key] && allStages[key].label && typeof allStages[key].label === 'string') {
acc[key] = allStages[key].label;
} else if (allStages[prefix] && allStages[prefix].label) {
if (this.allStages[key] && this.allStages[key].label && typeof this.allStages[key].label === 'string') {
acc[key] = this.allStages[key].label;
} else if (this.allStages[prefix] && this.allStages[prefix].label) {
acc[key] = suffix;
} else {
acc[key] = key;
Expand All @@ -505,7 +511,7 @@ export default class SankeyChart extends AbstractChart {
.map((flow) => flow.split('->'))
.flat()
.reduce((acc, key) => {
stages.forEach((stage, column) => {
this.stages.forEach((stage, column) => {
if (stage[key]) {
acc[key] = column + 1;
}
Expand Down Expand Up @@ -535,8 +541,8 @@ export default class SankeyChart extends AbstractChart {
label: 'My sankey',
data: this.enriched,
labels: this.labels,
colorFrom: ({ raw }) => allStages[raw.from.split(':')[0]]?.color || 'purple',
colorTo: ({ raw }) => allStages[raw.to.split(':')[0]]?.color || 'gray',
colorFrom: ({ raw }) => this.allStages[raw.from.split(':')[0]]?.color || 'purple',
colorTo: ({ raw }) => this.allStages[raw.to.split(':')[0]]?.color || 'gray',
columns: this.columns,
colorMode: 'gradient', // or 'from' or 'to'

Expand All @@ -549,7 +555,7 @@ export default class SankeyChart extends AbstractChart {

// eslint-disable-next-line class-methods-use-this
updateDataFacets(dataChunks) {
const facetcandidates = stages.filter((stage) => stage.label);
const facetcandidates = this.stages.filter((stage) => stage.label);

facetcandidates.forEach((facet) => {
dataChunks.addFacet(facet.label, (bundle) => Object.entries(facet)
Expand Down
10 changes: 9 additions & 1 deletion tools/oversight/elements/facetsidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default class FacetSidebar extends HTMLElement {
});
}

updateFacets(mode) {
updateFacets(mode, contenttype) {
const filterTags = document.querySelector('.filter-tags');
filterTags.textContent = '';
const addFilterTag = (name, value) => {
Expand Down Expand Up @@ -74,5 +74,13 @@ export default class FacetSidebar extends HTMLElement {
if (facetEl) facetEl.setAttribute('mode', mode || 'default');
if (facetEl) this.elems.facetsElement.append(facetEl);
});

// for the experiment sankey, hide variant facet if no experiment has been chosen for filtering
const variantFacet = document.getElementsByTagName('variant-list-facet')[0];
if (!contenttype?.startsWith('experiment:')) {
variantFacet.hidden = true;
} else {
variantFacet.hidden = false;
}
}
}
44 changes: 27 additions & 17 deletions tools/oversight/elements/list-facet.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,18 @@ export default class ListFacet extends HTMLElement {
if (this.dataChunks) this.update();
}

// eslint-disable-next-line class-methods-use-this
reorderFacetEntries(facetEntries) {
return facetEntries;
}

update() {
const facetName = this.getAttribute('facet');
const facetEntries = this.dataChunks.facets[facetName];
let facetEntries = this.dataChunks.facets[facetName];
const enabled = !this.closest('facet-sidebar[aria-disabled="true"]');

facetEntries = this.reorderFacetEntries(facetEntries);
const sort = this.getAttribute('sort') || 'count';

const optionKeys = facetEntries.map((f) => f.value)
.sort((a, b) => {
if (sort === 'count') return 0; // keep the order
Expand Down Expand Up @@ -211,6 +216,7 @@ export default class ListFacet extends HTMLElement {
}
return acc.slice(0, i);
}, entries[0].value);

entries.slice(start, end).forEach((entry) => {
const div = document.createElement('div');
const input = document.createElement('input');
Expand Down Expand Up @@ -281,21 +287,7 @@ export default class ListFacet extends HTMLElement {

label.append(valuespan, countspan, conversionspan);

const ul = document.createElement('ul');
ul.classList.add('cwv');

// display core web vital to facets
// add lcp
const lcpLI = this.createCWVChiclet(entry, 'lcp');
ul.append(lcpLI);

// add cls
const clsLI = this.createCWVChiclet(entry, 'cls');
ul.append(clsLI);

// add inp
const inpLI = this.createCWVChiclet(entry, 'inp');
ul.append(inpLI);
const ul = this.addVitalMetrics(entry);

div.append(input, label, ul);

Expand Down Expand Up @@ -423,4 +415,22 @@ export default class ListFacet extends HTMLElement {
window.setTimeout(fillEl, 0);
return li;
}

addVitalMetrics(entry) {
const ul = document.createElement('ul');
ul.classList.add('cwv');
// display core web vital to facets
// add lcp
const lcpLI = this.createCWVChiclet(entry, 'lcp');
ul.append(lcpLI);

// add cls
const clsLI = this.createCWVChiclet(entry, 'cls');
ul.append(clsLI);

// add inp
const inpLI = this.createCWVChiclet(entry, 'inp');
ul.append(inpLI);
return ul;
}
}
Loading
Loading