Skip to content
This repository was archived by the owner on Aug 2, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
c5377a5
Starting from scratch for the next version rewrite
sarveshnagpal Apr 2, 2019
d4fb759
Restructuring code and adding async support
sarveshnagpal Apr 8, 2019
502646a
Adding support for serialization
sarveshnagpal Jun 10, 2019
66f1c10
Updating layout computation code
sarveshnagpal Jun 12, 2019
1065a9d
Flatten dom serialization
sarveshnagpal Jun 13, 2019
888b888
Adding basic support for mutations
sarveshnagpal Jun 18, 2019
d537bc6
Adding basic support for deserialization
sarveshnagpal Jun 26, 2019
33a56bc
Adding basic support for mutations
Jul 5, 2019
58248df
Wiring up metrics code
sarveshnagpal Jul 8, 2019
cb57a1e
Reoganizing metrics and serialization code
Jul 10, 2019
80e9a42
Adding basic support for types and serialization
Jul 11, 2019
0de5378
Restructuring files
Jul 12, 2019
d01cef9
Restructing files
Jul 13, 2019
af71719
Adding support for viewport and mouse interactions
sarveshnagpal Jul 15, 2019
52850d5
Initialize various modules on clarity startup
Jul 16, 2019
66fb64f
Refactoring code to restructure core methods
Jul 30, 2019
b4fdc60
Adding early support for config
sarveshnagpal Aug 1, 2019
6675bbb
Isolating code to decode in a separate folder
sarveshnagpal Aug 2, 2019
da49bdf
Refactoring code to decode
sarveshnagpal Aug 5, 2019
bcf1729
Updating webpack configuration
sarveshnagpal Aug 5, 2019
b3aa40a
Streamlining configuration and updating decode imports
sarveshnagpal Aug 6, 2019
ca6e7bf
Removing console statements
Aug 6, 2019
61eca38
Remove console statements
Aug 6, 2019
c1693c9
Adding some debug statements and fixing tokens
sarveshnagpal Aug 7, 2019
aee28f2
Bug fixes and adding support for devtools tracking
Aug 7, 2019
e726d99
Bug fixes and adding decode support for viewport
sarveshnagpal Aug 8, 2019
ba80d1f
Special handling for style tags
sarveshnagpal Aug 8, 2019
e743748
Handling empty attributes
Aug 8, 2019
61a7b00
Adding svg support while decoding
sarveshnagpal Aug 9, 2019
5a11da2
Handling mutations on text node
Aug 9, 2019
5d28828
Bug fixes and visualization updates
sarveshnagpal Aug 10, 2019
0820c48
Refactoring code and adding support for metadata
sarveshnagpal Aug 11, 2019
ac36d62
Removing updated flag from modules
sarveshnagpal Aug 11, 2019
05bc6a8
Refactoring metrics to be uplevel field in payload
sarveshnagpal Aug 12, 2019
9007b78
Reogranizing envelope
Aug 13, 2019
889fd47
Simplifying metrics
sarveshnagpal Aug 13, 2019
5769da0
Adding more metrics
sarveshnagpal Aug 13, 2019
4636227
Metric presentation changes
Aug 14, 2019
d87003c
Adding scroll support to visualization
sarveshnagpal Aug 14, 2019
e57325d
Rename and refactoring
sarveshnagpal Aug 15, 2019
92336ca
Adding diagnostic support
sarveshnagpal Aug 15, 2019
2e2d0e4
Adding summary support
Aug 15, 2019
5bd64a6
Adding summary support
Aug 15, 2019
dcb6a3b
Breaking events into two queues
sarveshnagpal Aug 16, 2019
800e715
Renaming event queues
sarveshnagpal Aug 17, 2019
476600d
Sending additive payload for metrics
sarveshnagpal Aug 17, 2019
e88b2a0
Optimizing encoding
sarveshnagpal Aug 19, 2019
b96a510
Code refactor and adding index for each module
sarveshnagpal Aug 19, 2019
8bf9f37
Bug fixes to account for new encoding
sarveshnagpal Aug 20, 2019
7ada9a6
Getting rid of delta incrementing numbers
sarveshnagpal Aug 20, 2019
a7e7feb
Adding devtools support & masking text by default
sarveshnagpal Aug 20, 2019
abfebf4
Refactoring code to remove viewport directory
sarveshnagpal Aug 21, 2019
a23af3c
Simplifying events and improving text masking
sarveshnagpal Aug 22, 2019
2ffe9c1
Removing debug statements
sarveshnagpal Aug 23, 2019
88d5dec
Separate logic to decode JSON and render JSON
sarveshnagpal Aug 24, 2019
700dbb7
Refactoring dom directory to layout
sarveshnagpal Aug 24, 2019
dd0dc9d
Adding basic support for box model
sarveshnagpal Aug 26, 2019
2618125
Simplifying SVG handling
sarveshnagpal Aug 26, 2019
e781f18
Bug fix related to style mutations
sarveshnagpal Aug 26, 2019
e37779e
Serializing async call execution
sarveshnagpal Aug 27, 2019
8325c1f
Adding upload support & getting rid of two streams
Aug 27, 2019
417e515
Ability to set ids via configurations
Aug 27, 2019
d08d51e
Bugfix: Maintaining order during complex mutations
sarveshnagpal Aug 28, 2019
75de7a8
Adding support for insertRule and deleteRule
sarveshnagpal Aug 28, 2019
9261b33
Default to masking everything
Aug 28, 2019
e19dd3d
Merge branch 'sarveshn/vnext' of https://github.com/Microsoft/clarity…
Aug 28, 2019
4820419
Adding support for node level masking
sarveshnagpal Aug 28, 2019
11ddc1c
Merge branch 'sarveshn/vnext' of https://github.com/Microsoft/clarity…
sarveshnagpal Aug 28, 2019
b290159
Adding support for unmasking attribute
sarveshnagpal Aug 28, 2019
6d28ea3
Rendering mitigation and adding IMG as leaf node
sarveshnagpal Aug 29, 2019
28ce87d
Layout rendering fixes
Aug 31, 2019
94e2334
Simplifying metrics
sarveshnagpal Aug 31, 2019
18e6258
Deleting unused code
sarveshnagpal Sep 1, 2019
58e9118
Adding support for checksum and some refactoring
sarveshnagpal Sep 2, 2019
07895ef
Supporting boxmodel visualization
sarveshnagpal Sep 2, 2019
6f7513e
Adding support for text selection
sarveshnagpal Sep 3, 2019
c832984
Adding support for mouse playback
Sep 4, 2019
0e6754c
Refactoring code to mask as a core function
sarveshnagpal Sep 4, 2019
a68813f
Major refactoring to remove event groupings
sarveshnagpal Sep 4, 2019
2579848
Simplifying task manager and layout module
Sep 5, 2019
aac746d
Fixing playback and completing refactoring
sarveshnagpal Sep 5, 2019
5a85a4a
Box model replay alignment
sarveshnagpal Sep 5, 2019
867075b
Adding support for change event and lean mode
Sep 6, 2019
348e2a2
Rendering fixes to honor box model after mutation
sarveshnagpal Sep 7, 2019
a7e6237
Wiring unload handler and some more refactoring
sarveshnagpal Sep 8, 2019
0668bdd
Some rendering improvements
sarveshnagpal Sep 8, 2019
7d28b58
Unmasking bugfix
sarveshnagpal Sep 9, 2019
68905d5
Metric rendering bug fix and sending end signal
sarveshnagpal Sep 9, 2019
546a373
Handling dom removals
sarveshnagpal Sep 10, 2019
4261f79
Stateless decoding
sarveshnagpal Sep 10, 2019
edbd844
Splitting events into 3 streams at decoding time
sarveshnagpal Sep 10, 2019
39eb5c0
Renaming ChecksumMap to LayoutSummary
sarveshnagpal Sep 10, 2019
80b9380
Simplifying unique ids and cookie tracking
sarveshnagpal Sep 11, 2019
df121c6
Refactor pointer and support for custom selector
sarveshnagpal Sep 11, 2019
e4814fa
1.0.0-beta.0
sarveshnagpal Sep 11, 2019
65467d4
Simplifying decode logic
Sep 12, 2019
99cb6e2
Adding support for ping & pause / restart
Sep 13, 2019
9ebfea9
Changing package exports
Sep 13, 2019
f805562
Supporting pagehide and checking for promise
sarveshnagpal Sep 15, 2019
148aec7
Clearing timeouts while ending Clarity session
sarveshnagpal Sep 18, 2019
d1b4aa5
Simplifying metrics and updating version (#148)
sarveshnagpal Sep 23, 2019
4556495
vNextBeta6: Type refactoring and addressing known issues (#149)
sarveshnagpal Sep 30, 2019
964da6b
Resolving duplicate Ids using WeakMap (#150)
sarveshnagpal Oct 4, 2019
8815a85
Bug fixes and data quality improvements over vBeta7 (#151)
sarveshnagpal Oct 13, 2019
9816161
Incremental bug fixes over v1.0.0-beta8 (#153)
sarveshnagpal Oct 18, 2019
d590fd4
Bug fixes and feature additions in v1.0.0-beta.10 (#154)
sarveshnagpal Oct 21, 2019
80fd071
adds flags to disable clarity tracking in certain interface elements
saiphcita Oct 30, 2019
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

# Build results
build/
clarity.js
decode.js

# npm packages
.rpt2_cache

# npm packages
node_modules
Expand Down
225 changes: 225 additions & 0 deletions decode/clarity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import version from "../src/core/version";
import { Event, Payload, Token } from "../types/data";
import { MetricEvent, PageEvent, PingEvent, SummaryEvent, TagEvent, UploadEvent } from "../types/decode/data";
import { DecodedEvent, DecodedPayload } from "../types/decode/decode";
import { ImageErrorEvent, ScriptErrorEvent } from "../types/decode/diagnostic";
import { InputChangeEvent, PointerEvent, ResizeEvent, ScrollEvent } from "../types/decode/interaction";
import { SelectionEvent, UnloadEvent, VisibilityEvent } from "../types/decode/interaction";
import { BoxModelEvent, DocumentEvent, DomEvent, HashEvent, ResourceEvent, TargetEvent } from "../types/decode/layout";

import * as data from "./data";
import * as diagnostic from "./diagnostic";
import * as interaction from "./interaction";
import * as layout from "./layout";
import * as r from "./render";

let pageId: string = null;

export function decode(input: string): DecodedPayload {
let json: Payload = typeof input === "string" ? JSON.parse(input) : input;
let envelope = data.envelope(json.e);
let timestamp = Date.now();
let payload: DecodedPayload = { timestamp, envelope };
let encoded: Token[][] = json.d;

if (payload.envelope.version !== version) {
throw new Error(`Invalid Clarity Version. Actual: ${payload.envelope.version} | Expected: ${version} | ${input.substr(0, 250)}`);
}

/* Reset components before decoding to keep them stateless */
data.reset();
layout.reset();

for (let entry of encoded) {
data.summarize(entry);
switch (entry[1]) {
case Event.Page:
if (payload.page === undefined) { payload.page = []; }
payload.page.push(data.decode(entry) as PageEvent);
break;
case Event.Ping:
if (payload.ping === undefined) { payload.ping = []; }
payload.ping.push(data.decode(entry) as PingEvent);
break;
case Event.Tag:
if (payload.tag === undefined) { payload.tag = []; }
payload.tag.push(data.decode(entry) as TagEvent);
break;
case Event.Metric:
if (payload.metric === undefined) { payload.metric = []; }
payload.metric.push(data.decode(entry) as MetricEvent);
break;
case Event.Upload:
if (payload.upload === undefined) { payload.upload = []; }
payload.upload.push(data.decode(entry) as UploadEvent);
break;
case Event.MouseDown:
case Event.MouseUp:
case Event.MouseMove:
case Event.MouseWheel:
case Event.Click:
case Event.DoubleClick:
case Event.RightClick:
case Event.TouchStart:
case Event.TouchCancel:
case Event.TouchEnd:
case Event.TouchMove:
if (payload.pointer === undefined) { payload.pointer = []; }
payload.pointer.push(interaction.decode(entry) as PointerEvent);
break;
case Event.Scroll:
if (payload.scroll === undefined) { payload.scroll = []; }
payload.scroll.push(interaction.decode(entry) as ScrollEvent);
break;
case Event.Resize:
if (payload.resize === undefined) { payload.resize = []; }
payload.resize.push(interaction.decode(entry) as ResizeEvent);
break;
case Event.Selection:
if (payload.selection === undefined) { payload.selection = []; }
payload.selection.push(interaction.decode(entry) as SelectionEvent);
break;
case Event.InputChange:
if (payload.input === undefined) { payload.input = []; }
payload.input.push(interaction.decode(entry) as InputChangeEvent);
break;
case Event.Unload:
if (payload.unload === undefined) { payload.unload = []; }
payload.unload.push(interaction.decode(entry) as UnloadEvent);
break;
case Event.Visibility:
if (payload.visibility === undefined) { payload.visibility = []; }
payload.visibility.push(interaction.decode(entry) as VisibilityEvent);
break;
case Event.Target:
if (payload.target === undefined) { payload.target = []; }
payload.target.push(layout.decode(entry) as TargetEvent);
break;
case Event.BoxModel:
if (payload.boxmodel === undefined) { payload.boxmodel = []; }
payload.boxmodel.push(layout.decode(entry) as BoxModelEvent);
break;
case Event.Discover:
case Event.Mutation:
if (payload.dom === undefined) { payload.dom = []; }
payload.dom.push(layout.decode(entry) as DomEvent);
break;
case Event.Hash:
if (payload.hash === undefined) { payload.hash = []; }
payload.hash.push(layout.decode(entry) as HashEvent);
break;
case Event.Document:
if (payload.doc === undefined) { payload.doc = []; }
payload.doc.push(layout.decode(entry) as DocumentEvent);
break;
case Event.ScriptError:
if (payload.script === undefined) { payload.script = []; }
payload.script.push(diagnostic.decode(entry) as ScriptErrorEvent);
break;
case Event.ImageError:
if (payload.image === undefined) { payload.image = []; }
payload.image.push(diagnostic.decode(entry) as ImageErrorEvent);
break;
default:
console.error(`No handler for Event: ${JSON.stringify(entry)}`);
break;
}
}

/* Enrich decoded payload with derived events */
payload.summary = data.summary() as SummaryEvent[];
if (payload.dom && payload.dom.length > 0) { payload.hash = layout.hash() as HashEvent[]; }
if (layout.resources.length > 0) { payload.resource = layout.resource() as ResourceEvent[]; }

return payload;
}

export function html(decoded: DecodedPayload): string {
let iframe = document.createElement("iframe");
render(decoded, iframe);
return iframe.contentDocument.documentElement.outerHTML;
}

export function render(decoded: DecodedPayload, iframe: HTMLIFrameElement, header?: HTMLElement): void {
// Reset rendering if we receive a new pageId
if (pageId !== decoded.envelope.pageId) {
pageId = decoded.envelope.pageId;
r.reset();
}

// Replay events
let events: DecodedEvent[] = [];
for (let key in decoded) {
if (Array.isArray(decoded[key])) {
events = events.concat(decoded[key]);
}
}
replay(events.sort(sort), iframe, header);
}

export async function replay(events: DecodedEvent[], iframe: HTMLIFrameElement, header?: HTMLElement): Promise<void> {
let start = events[0].time;
for (let entry of events) {
if (entry.time - start > 16) { start = await wait(entry.time); }

switch (entry.event) {
case Event.Page:
let pageEvent = entry as PageEvent;
r.page(pageEvent.data, iframe);
break;
case Event.Metric:
let metricEvent = entry as MetricEvent;
if (header) { r.metric(metricEvent.data, header); }
break;
case Event.Discover:
case Event.Mutation:
let domEvent = entry as DomEvent;
r.markup(domEvent.data, iframe);
break;
case Event.BoxModel:
let boxModelEvent = entry as BoxModelEvent;
r.boxmodel(boxModelEvent.data, iframe);
break;
case Event.MouseDown:
case Event.MouseUp:
case Event.MouseMove:
case Event.MouseWheel:
case Event.Click:
case Event.DoubleClick:
case Event.RightClick:
case Event.TouchStart:
case Event.TouchCancel:
case Event.TouchEnd:
case Event.TouchMove:
let pointerEvent = entry as PointerEvent;
r.pointer(pointerEvent.event, pointerEvent.data, iframe);
break;
case Event.InputChange:
let changeEvent = entry as InputChangeEvent;
r.change(changeEvent.data, iframe);
break;
case Event.Selection:
let selectionEvent = entry as SelectionEvent;
r.selection(selectionEvent.data, iframe);
break;
case Event.Resize:
let resizeEvent = entry as ResizeEvent;
r.resize(resizeEvent.data, iframe);
break;
case Event.Scroll:
let scrollEvent = entry as ScrollEvent;
r.scroll(scrollEvent.data, iframe);
break;
}
}
}

async function wait(timestamp: number): Promise<number> {
return new Promise<number>((resolve: FrameRequestCallback): void => {
setTimeout(resolve, 10, timestamp);
});
}

function sort(a: DecodedEvent, b: DecodedEvent): number {
return a.time - b.time;
}
79 changes: 79 additions & 0 deletions decode/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { BooleanFlag, Envelope, Event, MetricData, PageData, PingData } from "../types/data";
import { SummaryData, TagData, Token, Upload, UploadData } from "../types/data";
import { DataEvent } from "../types/decode/data";

let summaries: { [key: number]: SummaryData[] } = null;
const SUMMARY_THRESHOLD = 30;

export function reset(): void {
summaries = {};
}

export function decode(tokens: Token[]): DataEvent {
let time = tokens[0] as number;
let event = tokens[1] as Event;
switch (event) {
case Event.Page:
let page: PageData = {
timestamp: tokens[2] as number,
ua: tokens[3] as string,
url: tokens[4] as string,
referrer: tokens[5] as string,
lean: tokens[6] as BooleanFlag,
};
return { time, event, data: page };
case Event.Ping:
let ping: PingData = { gap: tokens[2] as number };
return { time, event, data: ping };
case Event.Tag:
let tag: TagData = { key: tokens[2] as string, value: tokens[3] as string };
return { time, event, data: tag };
case Event.Upload:
let upload: UploadData = { sequence: tokens[2] as number, attempts: tokens[3] as number, status: tokens[4] as number};
return { time, event, data: upload };
case Event.Metric:
let i = 2; // Start from 3rd index since first two are used for time & event
let metrics: MetricData = {};
while (i < tokens.length) {
metrics[tokens[i++] as number] = tokens[i++] as number;
}
return { time, event, data: metrics };
}
}

export function envelope(tokens: Token[]): Envelope {
return {
sequence: tokens[0] as number,
version: tokens[1] as string,
projectId: tokens[2] as string,
userId: tokens[3] as string,
sessionId: tokens[4] as string,
pageId: tokens[5] as string,
upload: tokens[6] as Upload,
end: tokens[7] as BooleanFlag
};
}

export function summarize(entry: Token[]): void {
let time = entry[0] as number;
let type = entry[1] as Event;
let data: SummaryData = { event: type, start: time, end: time };
if (!(type in summaries)) { summaries[type] = [data]; }

let s = summaries[type][summaries[type].length - 1];
if (time - s.end < SUMMARY_THRESHOLD) { s.end = time; } else { summaries[type].push(data); }
}

export function summary(): DataEvent[] {
let data: SummaryData[] = [];
let time = null;
for (let type in summaries) {
if (summaries[type]) {
for (let d of summaries[type]) {
time = time ? Math.min(time, d.start) : d.start;
data.push(d);
}
}
}
return data.length > 0 ? [{ time, event: Event.Summary, data }] : null;
}
25 changes: 25 additions & 0 deletions decode/diagnostic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Event, Token } from "../types/data";
import { DiagnosticEvent } from "../types/decode/diagnostic";
import { ImageErrorData, ScriptErrorData } from "../types/diagnostic";

export function decode(tokens: Token[]): DiagnosticEvent {
let time = tokens[0] as number;
let event = tokens[1] as Event;
switch (event) {
case Event.ImageError:
let imageError: ImageErrorData = {
source: tokens[2] as string,
target: tokens[3] as number
};
return { time, event, data: imageError };
case Event.ScriptError:
let scriptError: ScriptErrorData = {
message: tokens[3] as string,
line: tokens[4] as number,
column: tokens[5] as number,
stack: tokens[6] as string,
source: tokens[2] as string
};
return { time, event, data: scriptError };
}
}
1 change: 1 addition & 0 deletions decode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./clarity";
49 changes: 49 additions & 0 deletions decode/interaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Event, Token } from "../types/data";
import { InteractionEvent } from "../types/decode/interaction";
import { InputChangeData, PointerData, ResizeData, ScrollData, SelectionData, UnloadData, VisibilityData } from "../types/interaction";

export function decode(tokens: Token[]): InteractionEvent {
let time = tokens[0] as number;
let event = tokens[1] as Event;
switch (event) {
case Event.MouseDown:
case Event.MouseUp:
case Event.MouseMove:
case Event.MouseWheel:
case Event.Click:
case Event.DoubleClick:
case Event.RightClick:
case Event.TouchStart:
case Event.TouchCancel:
case Event.TouchEnd:
case Event.TouchMove:
let pointerData: PointerData = { target: tokens[2] as number, x: tokens[3] as number, y: tokens[4] as number };
return { time, event, data: pointerData };
case Event.Resize:
let resizeData: ResizeData = { width: tokens[2] as number, height: tokens[3] as number };
return { time, event, data: resizeData };
case Event.InputChange:
let changeData: InputChangeData = {
target: tokens[2] as number,
value: tokens[3] as string
};
return { time, event, data: changeData };
case Event.Selection:
let selectionData: SelectionData = {
start: tokens[2] as number,
startOffset: tokens[3] as number,
end: tokens[4] as number,
endOffset: tokens[5] as number
};
return { time, event, data: selectionData };
case Event.Scroll:
let scrollData: ScrollData = { target: tokens[2] as number, x: tokens[3] as number, y: tokens[4] as number };
return { time, event, data: scrollData };
case Event.Visibility:
let visibleData: VisibilityData = { visible: tokens[2] as string };
return { time, event, data: visibleData };
case Event.Unload:
let unloadData: UnloadData = { name: tokens[2] as string };
return { time, event, data: unloadData };
}
}
Loading