diff --git a/lerna.json b/lerna.json index a0ac9619..f24e9255 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.6.26", + "version": "0.6.27", "npmClient": "yarn", "useWorkspaces": true } diff --git a/package.json b/package.json index 7ef548c8..c65e6e60 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "clarity", "private": true, - "version": "0.6.26", + "version": "0.6.27", "repository": "https://github.com/microsoft/clarity.git", "author": "Sarvesh Nagpal ", "license": "MIT", diff --git a/packages/clarity-decode/package.json b/packages/clarity-decode/package.json index 606a023a..07c773a8 100644 --- a/packages/clarity-decode/package.json +++ b/packages/clarity-decode/package.json @@ -1,6 +1,6 @@ { "name": "clarity-decode", - "version": "0.6.26", + "version": "0.6.27", "description": "An analytics library that uses web page interactions to generate aggregated insights", "author": "Microsoft Corp.", "license": "MIT", @@ -26,7 +26,7 @@ "url": "https://github.com/Microsoft/clarity/issues" }, "dependencies": { - "clarity-js": "^0.6.26" + "clarity-js": "^0.6.27" }, "devDependencies": { "@rollup/plugin-commonjs": "^19.0.1", diff --git a/packages/clarity-decode/src/clarity.ts b/packages/clarity-decode/src/clarity.ts index 52f8d8b5..9380e3f2 100644 --- a/packages/clarity-decode/src/clarity.ts +++ b/packages/clarity-decode/src/clarity.ts @@ -1,11 +1,11 @@ import { Data, version } from "clarity-js"; import { BaselineEvent, CustomEvent, DecodedPayload, DecodedVersion, DimensionEvent } from "../types/data"; import { LimitEvent, MetricEvent, PingEvent, SummaryEvent, UpgradeEvent, UploadEvent, VariableEvent } from "../types/data"; -import { ImageErrorEvent, LogEvent, ScriptErrorEvent } from "../types/diagnostic"; -import { ClickEvent, InputEvent, PointerEvent, ResizeEvent, ScrollEvent, TimelineEvent } from "../types/interaction"; +import { LogEvent, ScriptErrorEvent } from "../types/diagnostic"; +import { ClickEvent, ClipboardEvent, InputEvent, PointerEvent, ResizeEvent, ScrollEvent, SubmitEvent, TimelineEvent } from "../types/interaction"; import { SelectionEvent, UnloadEvent, VisibilityEvent } from "../types/interaction"; import { BoxEvent, DocumentEvent, DomEvent, RegionEvent } from "../types/layout"; -import { ConnectionEvent, NavigationEvent } from "../types/performance"; +import { NavigationEvent } from "../types/performance"; import * as data from "./data"; import * as diagnostic from "./diagnostic"; @@ -101,6 +101,11 @@ export function decode(input: string): DecodedPayload { let clickEntry = interaction.decode(entry) as ClickEvent; payload.click.push(clickEntry); break; + case Data.Event.Clipboard: + if (payload.clipboard === undefined) { payload.clipboard = []; } + let clipEntry = interaction.decode(entry) as ClipboardEvent; + payload.clipboard.push(clipEntry); + break; case Data.Event.Scroll: if (payload.scroll === undefined) { payload.scroll = []; } payload.scroll.push(interaction.decode(entry) as ScrollEvent); @@ -113,6 +118,11 @@ export function decode(input: string): DecodedPayload { if (payload.selection === undefined) { payload.selection = []; } payload.selection.push(interaction.decode(entry) as SelectionEvent); break; + case Data.Event.Submit: + if (payload.submit === undefined) { payload.submit = []; } + let submitEntry = interaction.decode(entry) as SubmitEvent; + payload.submit.push(submitEntry); + break; case Data.Event.Timeline: if (payload.timeline === undefined) { payload.timeline = []; } payload.timeline.push(interaction.decode(entry) as TimelineEvent); @@ -150,18 +160,10 @@ export function decode(input: string): DecodedPayload { if (payload.script === undefined) { payload.script = []; } payload.script.push(diagnostic.decode(entry) as ScriptErrorEvent); break; - case Data.Event.ImageError: - if (payload.image === undefined) { payload.image = []; } - payload.image.push(diagnostic.decode(entry) as ImageErrorEvent); - break; case Data.Event.Log: if (payload.log === undefined) { payload.log = []; } payload.log.push(diagnostic.decode(entry) as LogEvent); break; - case Data.Event.Connection: - if (payload.connection === undefined) { payload.connection = []; } - payload.connection.push(performance.decode(entry) as ConnectionEvent); - break; case Data.Event.Navigation: if (payload.navigation === undefined) { payload.navigation = []; } payload.navigation.push(performance.decode(entry) as NavigationEvent); diff --git a/packages/clarity-decode/src/diagnostic.ts b/packages/clarity-decode/src/diagnostic.ts index 89d8211b..722dcf00 100644 --- a/packages/clarity-decode/src/diagnostic.ts +++ b/packages/clarity-decode/src/diagnostic.ts @@ -5,12 +5,6 @@ export function decode(tokens: Data.Token[]): DiagnosticEvent { let time = tokens[0] as number; let event = tokens[1] as Data.Event; switch (event) { - case Data.Event.ImageError: - let imageError: Diagnostic.ImageErrorData = { - source: tokens[2] as string, - target: tokens[3] as number, - }; - return { time, event, data: imageError }; case Data.Event.ScriptError: let scriptError: Diagnostic.ScriptErrorData = { message: tokens[2] as string, diff --git a/packages/clarity-decode/src/interaction.ts b/packages/clarity-decode/src/interaction.ts index 80d15282..da158f64 100644 --- a/packages/clarity-decode/src/interaction.ts +++ b/packages/clarity-decode/src/interaction.ts @@ -37,6 +37,9 @@ export function decode(tokens: Data.Token[]): InteractionEvent { hashBeta: clickHashes.length > 0 ? clickHashes[1] : null }; return { time, event, data: clickData }; + case Data.Event.Clipboard: + let clipData: Interaction.ClipboardData = { target: tokens[2] as number, action: tokens[3] as Interaction.Clipboard }; + return { time, event, data: clipData }; case Data.Event.Resize: let resizeData: Interaction.ResizeData = { width: tokens[2] as number, height: tokens[3] as number }; return { time, event, data: resizeData }; @@ -54,6 +57,11 @@ export function decode(tokens: Data.Token[]): InteractionEvent { endOffset: tokens[5] as number }; return { time, event, data: selectionData }; + case Data.Event.Submit: + let submitData: Interaction.SubmitData = { + target: tokens[2] as number + }; + return { time, event, data: submitData }; case Data.Event.Scroll: let scrollData: Interaction.ScrollData = { target: tokens[2] as number, diff --git a/packages/clarity-decode/src/performance.ts b/packages/clarity-decode/src/performance.ts index 507f26b1..c243f483 100644 --- a/packages/clarity-decode/src/performance.ts +++ b/packages/clarity-decode/src/performance.ts @@ -5,14 +5,6 @@ export function decode(tokens: Data.Token[]): PerformanceEvent { let time = tokens[0] as number; let event = tokens[1] as Data.Event; switch (event) { - case Data.Event.Connection: - let connectionData: Performance.ConnectionData = { - downlink: tokens[2] as number, - rtt: tokens[3] as number, - saveData: tokens[4] as number, - type: tokens[5] as string - }; - return { time, event, data: connectionData }; case Data.Event.Navigation: let navigationData: Performance.NavigationData = { fetchStart: tokens[2] as number, diff --git a/packages/clarity-decode/types/data.d.ts b/packages/clarity-decode/types/data.d.ts index 5805894c..3c2851cb 100644 --- a/packages/clarity-decode/types/data.d.ts +++ b/packages/clarity-decode/types/data.d.ts @@ -1,9 +1,9 @@ import { Data } from "clarity-js"; -import { DiagnosticEvent, ImageErrorEvent, LogEvent, ScriptErrorEvent } from "./diagnostic"; -import { ClickEvent, InputEvent, InteractionEvent, PointerEvent, ResizeEvent } from "./interaction"; +import { DiagnosticEvent, LogEvent, ScriptErrorEvent } from "./diagnostic"; +import { ClickEvent, ClipboardEvent, InputEvent, InteractionEvent, PointerEvent, ResizeEvent, SubmitEvent } from "./interaction"; import { ScrollEvent, SelectionEvent, TimelineEvent, UnloadEvent, VisibilityEvent } from "./interaction"; import { BoxEvent, DocumentEvent, DomEvent, LayoutEvent, RegionEvent } from "./layout"; -import { ConnectionEvent, NavigationEvent, PerformanceEvent } from "./performance"; +import { NavigationEvent, PerformanceEvent } from "./performance"; import { PartialEvent } from "./core"; /* Redeclare enums */ @@ -50,14 +50,15 @@ export interface DecodedPayload { dimension?: DimensionEvent[]; ping?: PingEvent[]; limit?: LimitEvent[]; - image?: ImageErrorEvent[]; script?: ScriptErrorEvent[]; input?: InputEvent[]; pointer?: PointerEvent[]; click?: ClickEvent[]; + clipboard?: ClipboardEvent[]; resize?: ResizeEvent[]; scroll?: ScrollEvent[]; selection?: SelectionEvent[]; + submit?: SubmitEvent[]; summary?: SummaryEvent[]; timeline?: TimelineEvent[]; unload?: UnloadEvent[]; @@ -68,7 +69,6 @@ export interface DecodedPayload { region?: RegionEvent[]; dom?: DomEvent[]; doc?: DocumentEvent[]; - connection?: ConnectionEvent[]; navigation?: NavigationEvent[]; log?: LogEvent[]; baseline?: BaselineEvent[]; diff --git a/packages/clarity-decode/types/diagnostic.d.ts b/packages/clarity-decode/types/diagnostic.d.ts index 660ac8e9..a0c3b58c 100644 --- a/packages/clarity-decode/types/diagnostic.d.ts +++ b/packages/clarity-decode/types/diagnostic.d.ts @@ -1,9 +1,8 @@ import { Diagnostic } from "clarity-js"; import { PartialEvent } from "./core"; -export interface ImageErrorEvent extends PartialEvent { data: Diagnostic.ImageErrorData; } export interface ScriptErrorEvent extends PartialEvent { data: Diagnostic.ScriptErrorData; } export interface LogEvent extends PartialEvent { data: Diagnostic.LogData; } export interface DiagnosticEvent extends PartialEvent { - data: Diagnostic.ImageErrorData | Diagnostic.LogData | Diagnostic.ScriptErrorData; + data: Diagnostic.LogData | Diagnostic.ScriptErrorData; } diff --git a/packages/clarity-decode/types/interaction.d.ts b/packages/clarity-decode/types/interaction.d.ts index 82af592f..92466472 100644 --- a/packages/clarity-decode/types/interaction.d.ts +++ b/packages/clarity-decode/types/interaction.d.ts @@ -11,20 +11,24 @@ export interface TimelineData extends Interaction.TimelineData { export interface InputEvent extends PartialEvent { data: Interaction.InputData; } export interface ClickEvent extends PartialEvent { data: ClickData; } +export interface ClipboardEvent extends PartialEvent { data: Interaction.ClipboardData; } export interface PointerEvent extends PartialEvent { data: Interaction.PointerData; } export interface ResizeEvent extends PartialEvent { data: Interaction.ResizeData; } export interface ScrollEvent extends PartialEvent { data: Interaction.ScrollData; } export interface SelectionEvent extends PartialEvent { data: Interaction.SelectionData; } +export interface SubmitEvent extends PartialEvent { data: Interaction.SubmitData; } export interface TimelineEvent extends PartialEvent { data: TimelineData; } export interface UnloadEvent extends PartialEvent { data: Interaction.UnloadData; } export interface VisibilityEvent extends PartialEvent { data: Interaction.VisibilityData; } export interface InteractionEvent extends PartialEvent { data: ClickData | + Interaction.ClipboardData | Interaction.InputData | Interaction.PointerData | Interaction.ResizeData | Interaction.ScrollData | Interaction.SelectionData | + Interaction.SubmitData | TimelineData | Interaction.UnloadData | Interaction.VisibilityData; diff --git a/packages/clarity-decode/types/performance.d.ts b/packages/clarity-decode/types/performance.d.ts index 0aeff90c..a6b8d159 100644 --- a/packages/clarity-decode/types/performance.d.ts +++ b/packages/clarity-decode/types/performance.d.ts @@ -1,8 +1,7 @@ import { Performance } from "clarity-js"; import { PartialEvent } from "./core"; -export interface ConnectionEvent extends PartialEvent { data: Performance.ConnectionData; } export interface NavigationEvent extends PartialEvent { data: Performance.NavigationData; } export interface PerformanceEvent extends PartialEvent { - data: Performance.ConnectionData | Performance.NavigationData + data: Performance.NavigationData } diff --git a/packages/clarity-devtools/package.json b/packages/clarity-devtools/package.json index fcac371e..2d89256d 100644 --- a/packages/clarity-devtools/package.json +++ b/packages/clarity-devtools/package.json @@ -1,6 +1,6 @@ { "name": "clarity-devtools", - "version": "0.6.26", + "version": "0.6.27", "private": true, "description": "Adds Clarity debugging support to browser devtools", "author": "Microsoft Corp.", @@ -24,9 +24,9 @@ "url": "https://github.com/Microsoft/clarity/issues" }, "dependencies": { - "clarity-decode": "^0.6.26", - "clarity-js": "^0.6.26", - "clarity-visualize": "^0.6.26" + "clarity-decode": "^0.6.27", + "clarity-js": "^0.6.27", + "clarity-visualize": "^0.6.27" }, "devDependencies": { "@rollup/plugin-node-resolve": "^13.0.2", @@ -39,8 +39,8 @@ "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.30.0", "source-map-loader": "^3.0.0", - "tslib": "^2.3.0", "ts-node": "^10.1.0", + "tslib": "^2.3.0", "tslint": "^6.1.3", "typescript": "^4.3.5" }, diff --git a/packages/clarity-devtools/src/config.ts b/packages/clarity-devtools/src/config.ts index 4b222a73..13c14508 100644 --- a/packages/clarity-devtools/src/config.ts +++ b/packages/clarity-devtools/src/config.ts @@ -17,6 +17,9 @@ export default function(): Core.Config { [Data.Metric.CartDiscount, 0, "span[data-checkout-discount-amount-target]", 100], /* 0: DOM Text */ [Data.Metric.ProductPrice, 1, "Analytics.product.price", 100], /* 1: Javascript */ [Data.Metric.CartTotal, 2, "data-checkout-payment-due-target"], /* 2: DOM Attribute */ + ], + dimensions: [ + [Data.Dimension.ProductBrand, 0, ".productBrand"], /* 0: DOM Text */ ] }; } diff --git a/packages/clarity-devtools/src/content.ts b/packages/clarity-devtools/src/content.ts index dbde436b..0fcf9e45 100644 --- a/packages/clarity-devtools/src/content.ts +++ b/packages/clarity-devtools/src/content.ts @@ -32,7 +32,13 @@ function setup(url: string): void { chrome.storage.sync.get({ clarity: { showText: true, leanMode: false } }, (items: any) => { let c = config(); let script = document.createElement("script"); - script.innerText = wireup({regions: c.regions, metrics: c.metrics, showText: items.clarity.showText, leanMode: items.clarity.leanMode}); + script.innerText = wireup({ + regions: c.regions, + metrics: c.metrics, + dimensions: c.dimensions, + showText: items.clarity.showText, + leanMode: items.clarity.leanMode + }); document.body.appendChild(script); }); break; @@ -53,6 +59,7 @@ function wireup(settings: any): string { lean: "$__leanMode__$", regions: "$__regions__$", metrics: "$__metrics__$", + dimensions: "$__dimensions__$", content: "$__showText__$", upload: (data: string): void => { window.postMessage({ action: "upload", payload: data }, "*"); }, projectId: "devtools" diff --git a/packages/clarity-devtools/static/manifest.json b/packages/clarity-devtools/static/manifest.json index 5e900f08..53920d97 100644 --- a/packages/clarity-devtools/static/manifest.json +++ b/packages/clarity-devtools/static/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "Clarity Developer Tools", "description": "Get insights about how customers use your website.", - "version": "0.6.26", - "version_name": "0.6.26", + "version": "0.6.27", + "version_name": "0.6.27", "minimum_chrome_version": "50", "devtools_page": "devtools.html", "icons": { diff --git a/packages/clarity-js/package.json b/packages/clarity-js/package.json index 26b43e81..cb58859c 100644 --- a/packages/clarity-js/package.json +++ b/packages/clarity-js/package.json @@ -1,6 +1,6 @@ { "name": "clarity-js", - "version": "0.6.26", + "version": "0.6.27", "description": "An analytics library that uses web page interactions to generate aggregated insights", "author": "Microsoft Corp.", "license": "MIT", diff --git a/packages/clarity-js/src/core/config.ts b/packages/clarity-js/src/core/config.ts index ec021294..e7a2150b 100644 --- a/packages/clarity-js/src/core/config.ts +++ b/packages/clarity-js/src/core/config.ts @@ -10,6 +10,7 @@ let config: Config = { unmask: [], regions: [], metrics: [], + dimensions: [], cookies: [], report: null, upload: null, diff --git a/packages/clarity-js/src/core/version.ts b/packages/clarity-js/src/core/version.ts index b63bf02b..889229dd 100644 --- a/packages/clarity-js/src/core/version.ts +++ b/packages/clarity-js/src/core/version.ts @@ -1,2 +1,2 @@ -let version = "0.6.26"; +let version = "0.6.27"; export default version; diff --git a/packages/clarity-js/src/data/metadata.ts b/packages/clarity-js/src/data/metadata.ts index f0f848eb..c91a3d90 100644 --- a/packages/clarity-js/src/data/metadata.ts +++ b/packages/clarity-js/src/data/metadata.ts @@ -41,11 +41,13 @@ export function start(): void { if (navigator) { dimension.log(Dimension.Language, (navigator).userLanguage || navigator.language); + metric.max(Metric.Automation, navigator.webdriver ? BooleanFlag.True : BooleanFlag.False); } // Metrics metric.max(Metric.ClientTimestamp, s.ts); metric.max(Metric.Playback, BooleanFlag.False); + if (screen) { metric.max(Metric.ScreenWidth, Math.round(screen.width)); metric.max(Metric.ScreenHeight, Math.round(screen.height)); diff --git a/packages/clarity-js/src/data/upload.ts b/packages/clarity-js/src/data/upload.ts index cae10b44..8686ab6d 100644 --- a/packages/clarity-js/src/data/upload.ts +++ b/packages/clarity-js/src/data/upload.ts @@ -56,6 +56,9 @@ export function queue(tokens: Token[], transmit: boolean = true): void { break; } + // Increment event count metric + metric.count(Metric.EventCount); + // Following two checks are precautionary and act as a fail safe mechanism to get out of unexpected situations. // Check 1: If for any reason the upload hasn't happened after waiting for 2x the config.delay time, // reset the timer. This allows Clarity to attempt an upload again. diff --git a/packages/clarity-js/src/diagnostic/encode.ts b/packages/clarity-js/src/diagnostic/encode.ts index 5fead84c..8bb09a6d 100644 --- a/packages/clarity-js/src/diagnostic/encode.ts +++ b/packages/clarity-js/src/diagnostic/encode.ts @@ -1,10 +1,8 @@ import { Event, Token } from "@clarity-types/data"; import { time } from "@src/core/time"; import { queue } from "@src/data/upload"; -import * as image from "@src/diagnostic/image"; import * as internal from "@src/diagnostic/internal"; import * as script from "@src/diagnostic/script"; -import { metadata } from "@src/layout/target"; export default async function (type: Event): Promise { let tokens: Token[] = [time(), type]; @@ -18,14 +16,6 @@ export default async function (type: Event): Promise { tokens.push(script.data.source); queue(tokens); break; - case Event.ImageError: - if (image.data) { - let imageTarget = metadata(image.data.target as Node, type); - tokens.push(image.data.source); - tokens.push(imageTarget.id); - queue(tokens); - } - break; case Event.Log: if (internal.data) { tokens.push(internal.data.code); diff --git a/packages/clarity-js/src/diagnostic/image.ts b/packages/clarity-js/src/diagnostic/image.ts deleted file mode 100644 index 072fe6bf..00000000 --- a/packages/clarity-js/src/diagnostic/image.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Event } from "@clarity-types/data"; -import { ImageErrorData } from "@clarity-types/diagnostic"; -import { bind } from "@src/core/event"; -import { schedule } from "@src/core/task"; -import encode from "./encode"; - -export let data: ImageErrorData; - -export function start(): void { - bind(document, "error", handler, true); -} - -function handler(error: ErrorEvent): void { - let element = error.target as HTMLElement; - if (element && element.tagName === "IMG") { - data = { source: (element as HTMLImageElement).src, target: element }; - schedule(encode.bind(this, Event.ImageError)); - } -} - -export function stop(): void { - data = null; -} diff --git a/packages/clarity-js/src/diagnostic/index.ts b/packages/clarity-js/src/diagnostic/index.ts index 41d6205c..358b0007 100644 --- a/packages/clarity-js/src/diagnostic/index.ts +++ b/packages/clarity-js/src/diagnostic/index.ts @@ -1,14 +1,11 @@ -import * as image from "./image"; import * as internal from "./internal"; import * as script from "./script"; export function start(): void { script.start(); - image.start(); internal.start(); } export function stop(): void { - image.stop(); internal.stop(); } diff --git a/packages/clarity-js/src/interaction/clipboard.ts b/packages/clarity-js/src/interaction/clipboard.ts new file mode 100644 index 00000000..6afac3af --- /dev/null +++ b/packages/clarity-js/src/interaction/clipboard.ts @@ -0,0 +1,32 @@ +import { Event } from "@clarity-types/data"; +import { Clipboard, ClipboardState } from "@clarity-types/interaction"; +import { bind } from "@src/core/event"; +import { schedule } from "@src/core/task"; +import { time } from "@src/core/time"; +import encode from "./encode"; +import { target } from "@src/layout/target"; + +export let state: ClipboardState[] = []; + +export function start(): void { + reset(); +} + +export function observe(root: Node): void { + bind(root, "cut", recompute.bind(this, Clipboard.Cut), true); + bind(root, "copy", recompute.bind(this, Clipboard.Copy), true); + bind(root, "paste", recompute.bind(this, Clipboard.Paste), true); +} + +function recompute(action: Clipboard, evt: UIEvent): void { + state.push({ time: time(), event: Event.Clipboard, data: { target: target(evt), action } }); + schedule(encode.bind(this, Event.Clipboard)); +} + +export function reset(): void { + state = []; +} + +export function stop(): void { + reset(); +} diff --git a/packages/clarity-js/src/interaction/encode.ts b/packages/clarity-js/src/interaction/encode.ts index 167f5ad3..0bb80e69 100644 --- a/packages/clarity-js/src/interaction/encode.ts +++ b/packages/clarity-js/src/interaction/encode.ts @@ -5,11 +5,13 @@ import * as baseline from "@src/data/baseline"; import { queue } from "@src/data/upload"; import { metadata } from "@src/layout/target"; import * as click from "./click"; +import * as clipboard from "./clipboard"; import * as input from "./input"; import * as pointer from "./pointer"; import * as resize from "./resize"; import * as scroll from "./scroll"; import * as selection from "./selection"; +import * as submit from "./submit"; import * as timeline from "./timeline"; import * as unload from "./unload"; import * as visibility from "./visibility"; @@ -27,8 +29,7 @@ export default async function (type: Event): Promise { case Event.TouchEnd: case Event.TouchMove: case Event.TouchCancel: - for (let i = 0; i < pointer.state.length; i++) { - let entry = pointer.state[i]; + for (let entry of pointer.state) { let pTarget = metadata(entry.data.target as Node, entry.event); if (pTarget.id > 0) { tokens = [entry.time, entry.event]; @@ -42,8 +43,7 @@ export default async function (type: Event): Promise { pointer.reset(); break; case Event.Click: - for (let i = 0; i < click.state.length; i++) { - let entry = click.state[i]; + for (let entry of click.state) { let cTarget = metadata(entry.data.target as Node, entry.event); tokens = [entry.time, entry.event]; let cHash = cTarget.hash.join(Constant.Dot); @@ -63,6 +63,18 @@ export default async function (type: Event): Promise { } click.reset(); break; + case Event.Clipboard: + for (let entry of clipboard.state) { + tokens = [entry.time, entry.event]; + let target = metadata(entry.data.target as Node, entry.event); + if (target.id > 0) { + tokens.push(target.id); + tokens.push(entry.data.action); + queue(tokens); + } + } + clipboard.reset(); + break; case Event.Resize: let r = resize.data; tokens.push(r.width); @@ -78,8 +90,7 @@ export default async function (type: Event): Promise { queue(tokens); break; case Event.Input: - for (let i = 0; i < input.state.length; i++) { - let entry = input.state[i]; + for (let entry of input.state) { let iTarget = metadata(entry.data.target as Node, entry.event); tokens = [entry.time, entry.event]; tokens.push(iTarget.id); @@ -102,8 +113,7 @@ export default async function (type: Event): Promise { } break; case Event.Scroll: - for (let i = 0; i < scroll.state.length; i++) { - let entry = scroll.state[i]; + for (let entry of scroll.state) { let sTarget = metadata(entry.data.target as Node, entry.event); if (sTarget.id > 0) { tokens = [entry.time, entry.event]; @@ -116,9 +126,19 @@ export default async function (type: Event): Promise { } scroll.reset(); break; + case Event.Submit: + for (let entry of submit.state) { + tokens = [entry.time, entry.event]; + let target = metadata(entry.data.target as Node, entry.event); + if (target.id > 0) { + tokens.push(target.id); + queue(tokens); + } + } + submit.reset(); + break; case Event.Timeline: - for (let i = 0; i < timeline.updates.length; i++) { - let entry = timeline.updates[i]; + for (let entry of timeline.updates) { tokens = [entry.time, entry.event]; tokens.push(entry.data.type); tokens.push(entry.data.hash); diff --git a/packages/clarity-js/src/interaction/index.ts b/packages/clarity-js/src/interaction/index.ts index c8a94adb..5d2d8405 100644 --- a/packages/clarity-js/src/interaction/index.ts +++ b/packages/clarity-js/src/interaction/index.ts @@ -1,9 +1,11 @@ import * as click from "@src/interaction/click"; +import * as clipboard from "@src/interaction/clipboard"; import * as input from "@src/interaction/input"; import * as pointer from "@src/interaction/pointer"; import * as resize from "@src/interaction/resize"; import * as scroll from "@src/interaction/scroll"; import * as selection from "@src/interaction/selection"; +import * as submit from "@src/interaction/submit"; import * as timeline from "@src/interaction/timeline"; import * as unload from "@src/interaction/unload"; import * as visibility from "@src/interaction/visibility"; @@ -11,24 +13,28 @@ import * as visibility from "@src/interaction/visibility"; export function start(): void { timeline.start(); click.start(); + clipboard.start(); pointer.start(); input.start(); resize.start(); visibility.start(); scroll.start(); selection.start(); + submit.start(); unload.start(); } export function stop(): void { timeline.stop(); click.stop(); + clipboard.stop(); pointer.stop(); input.stop(); resize.stop(); visibility.stop(); scroll.stop(); selection.stop(); + submit.stop(); unload.stop() } @@ -38,8 +44,10 @@ export function observe(root: Node): void { // In case of shadow DOM, following events automatically bubble up to the parent document. if (root.nodeType === Node.DOCUMENT_NODE) { click.observe(root); + clipboard.observe(root); pointer.observe(root); input.observe(root); selection.observe(root); + submit.observe(root); } -} \ No newline at end of file +} diff --git a/packages/clarity-js/src/interaction/submit.ts b/packages/clarity-js/src/interaction/submit.ts new file mode 100644 index 00000000..a3712810 --- /dev/null +++ b/packages/clarity-js/src/interaction/submit.ts @@ -0,0 +1,30 @@ +import { Event } from "@clarity-types/data"; +import { SubmitState } from "@clarity-types/interaction"; +import { bind } from "@src/core/event"; +import { schedule } from "@src/core/task"; +import { time } from "@src/core/time"; +import encode from "./encode"; +import { target } from "@src/layout/target"; + +export let state: SubmitState[] = []; + +export function start(): void { + reset(); +} + +export function observe(root: Node): void { + bind(root, "submit", recompute, true); +} + +function recompute(evt: UIEvent): void { + state.push({ time: time(), event: Event.Submit, data: { target: target(evt) } }); + schedule(encode.bind(this, Event.Submit)); +} + +export function reset(): void { + state = []; +} + +export function stop(): void { + reset(); +} diff --git a/packages/clarity-js/src/layout/dom.ts b/packages/clarity-js/src/layout/dom.ts index 22aff78f..4df98a5d 100644 --- a/packages/clarity-js/src/layout/dom.ts +++ b/packages/clarity-js/src/layout/dom.ts @@ -56,6 +56,7 @@ export function parse(root: ParentNode): void { if ("querySelectorAll" in root) { extract.regions(root, config.regions); extract.metrics(root, config.metrics); + extract.dimensions(root, config.dimensions); config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements config.unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements } diff --git a/packages/clarity-js/src/layout/extract.ts b/packages/clarity-js/src/layout/extract.ts index 9ca91514..754eb449 100644 --- a/packages/clarity-js/src/layout/extract.ts +++ b/packages/clarity-js/src/layout/extract.ts @@ -1,5 +1,6 @@ -import { Extract, Metric, Region, RegionFilter } from "@clarity-types/core"; -import { Constant } from "@clarity-types/data"; +import { Dimension, Extract, Metric, Region, RegionFilter } from "@clarity-types/core"; +import { Constant, Setting } from "@clarity-types/data"; +import * as dimension from "@src/data/dimension"; import * as metric from "@src/data/metric"; import * as region from "@src/layout/region"; @@ -33,6 +34,19 @@ export function metrics(root: ParentNode, value: Metric[]): void { } } +export function dimensions(root: ParentNode, value: Dimension[]): void { + for (let v of value) { + const [dimensionId, source, match] = v; + if (match) { + switch (source) { + case Extract.Text: root.querySelectorAll(match).forEach(e => { dimension.log(dimensionId, str((e as HTMLElement).innerText)); }); break; + case Extract.Attribute: root.querySelectorAll(`[${match}]`).forEach(e => { dimension.log(dimensionId, str(e.getAttribute(match))); }); break; + case Extract.Javascript: dimension.log(dimensionId, str(evaluate(match, Constant.String))); break; + } + } + } +} + function regex(match: string): RegExp { regexCache[match] = match in regexCache ? regexCache[match] : new RegExp(match); return regexCache[match]; @@ -52,6 +66,11 @@ function evaluate(variable: string, type: string = null, base: Object = window): return null; } +function str(input: string): string { + // Automatically trim string to max of Setting.DimensionLimit to avoid fetching long strings + return input ? input.substr(0, Setting.DimensionLimit) : input; +} + function num(text: string, scale: number, localize: boolean = true): number { try { scale = scale || 1; diff --git a/packages/clarity-js/src/layout/mutation.ts b/packages/clarity-js/src/layout/mutation.ts index 097676dc..0780d9ca 100644 --- a/packages/clarity-js/src/layout/mutation.ts +++ b/packages/clarity-js/src/layout/mutation.ts @@ -52,10 +52,14 @@ export function start(): void { return deleteRule.apply(this, arguments); }; - // Listening to attachShadow API - HTMLElement.prototype.attachShadow = function (): ShadowRoot { - return schedule(attachShadow.apply(this, arguments)) as ShadowRoot; - } + // Add a hook to attachShadow API calls + // In case we are unable to add a hook and browser throws an exception, + // reset attachShadow variable and resume processing like before + try { + HTMLElement.prototype.attachShadow = function (): ShadowRoot { + return schedule(attachShadow.apply(this, arguments)) as ShadowRoot; + } + } catch { attachShadow = null; } } export function observe(node: Node): void { diff --git a/packages/clarity-js/src/performance/connection.ts b/packages/clarity-js/src/performance/connection.ts deleted file mode 100644 index 84dd2701..00000000 --- a/packages/clarity-js/src/performance/connection.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { BooleanFlag, Event } from "@clarity-types/data"; -import { ConnectionData, NavigatorConnection } from "@clarity-types/performance"; -import encode from "./encode"; - -// Reference: https://wicg.github.io/netinfo/ -export let data: ConnectionData; - -export function start(): void { - // Check if the client supports Navigator.Connection: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/connection - // This is an experimental API so we go a bit deeper in our check and ensure that values returned are valid - if (navigator && - "connection" in navigator && - "downlink" in navigator["connection"] && - typeof navigator["connection"]["downlink"] === "number") { - (navigator["connection"] as NavigatorConnection).addEventListener("change", recompute); - recompute(); - } -} - -function recompute(): void { - let connection = navigator["connection"] as NavigatorConnection; - data = { - downlink: connection.downlink, - rtt: connection.rtt, - saveData: connection.saveData ? BooleanFlag.True : BooleanFlag.False, - type: connection.effectiveType - }; - encode(Event.Connection); -} - -export function reset(): void { - data = null; -} - -export function stop(): void { - reset(); -} diff --git a/packages/clarity-js/src/performance/encode.ts b/packages/clarity-js/src/performance/encode.ts index 64c7ab19..b0deafdf 100644 --- a/packages/clarity-js/src/performance/encode.ts +++ b/packages/clarity-js/src/performance/encode.ts @@ -1,21 +1,12 @@ import {Event, Token} from "@clarity-types/data"; import { time } from "@src/core/time"; import { queue } from "@src/data/upload"; -import * as connection from "@src/performance/connection"; import * as navigation from "@src/performance/navigation"; export default async function(type: Event): Promise { let t = time(); let tokens: Token[] = [t, type]; switch (type) { - case Event.Connection: - tokens.push(connection.data.downlink); - tokens.push(connection.data.rtt); - tokens.push(connection.data.saveData); - tokens.push(connection.data.type); - connection.reset(); - queue(tokens, false); - break; case Event.Navigation: tokens.push(navigation.data.fetchStart); tokens.push(navigation.data.connectStart); diff --git a/packages/clarity-js/src/performance/index.ts b/packages/clarity-js/src/performance/index.ts index 29f2edd9..d026ef50 100644 --- a/packages/clarity-js/src/performance/index.ts +++ b/packages/clarity-js/src/performance/index.ts @@ -1,15 +1,12 @@ -import * as connection from "@src/performance/connection"; import * as navigation from "@src/performance/navigation"; import * as observer from "@src/performance/observer"; export function start(): void { navigation.reset(); - connection.start(); observer.start(); } export function stop(): void { observer.stop(); - connection.stop(); navigation.reset(); } diff --git a/packages/clarity-js/types/core.d.ts b/packages/clarity-js/types/core.d.ts index 4e614f4f..ce26723e 100644 --- a/packages/clarity-js/types/core.d.ts +++ b/packages/clarity-js/types/core.d.ts @@ -5,6 +5,7 @@ type TaskResolve = () => void; type UploadCallback = (data: string) => void; type Region = [number /* RegionId */, string /* Query Selector */, RegionFilter? /* Region Filter */, string? /* Filter Text */]; type Metric = [Data.Metric /* MetricId */, Extract /* Extract Filter */, string /* Match Value */, number? /* Scale Factor */]; +type Dimension = [Data.Dimension /* DimensionId */, Extract /* Extract Filter */, string /* Match Value */]; /* Enum */ @@ -118,6 +119,7 @@ export interface Config { unmask?: string[]; regions?: Region[]; metrics?: Metric[]; + dimensions?: Dimension[]; cookies?: string[]; report?: string; upload?: string | UploadCallback; diff --git a/packages/clarity-js/types/data.d.ts b/packages/clarity-js/types/data.d.ts index 41dc1b02..b40b1ab8 100644 --- a/packages/clarity-js/types/data.d.ts +++ b/packages/clarity-js/types/data.d.ts @@ -39,14 +39,22 @@ export const enum Event { Input = 27, Visibility = 28, Navigation = 29, +/** + * @deprecated No longer support Network Connection + */ Connection = 30, ScriptError = 31, +/** + * @deprecated No longer support Image Error + */ ImageError = 32, Log = 33, Variable = 34, Limit = 35, Summary = 36, Box = 37, + Clipboard = 38, + Submit = 39 } export const enum Metric { @@ -74,7 +82,9 @@ export const enum Metric { CartShipping = 21, CartDiscount = 22, CartTax = 23, - CartTotal = 24 + CartTotal = 24, + EventCount = 25, + Automation = 26 } export const enum Dimension { @@ -100,7 +110,6 @@ export const enum Dimension { MetaType = 19, MetaTitle = 20, Generator = 21 - } export const enum Check { @@ -160,6 +169,7 @@ export const enum Setting { BoxPrecision = 100, // Up to 2 decimal points (e.g. 34.56) ResizeObserverThreshold = 15, // At least 15 characters before we attach a resize observer for the node ScriptErrorLimit = 5, // Do not send the same script error more than 5 times per page + DimensionLimit = 256, // Do not extract dimensions which are over 256 characters WordLength = 5, // Estimated average size of a word, RestartDelay = 250, // Wait for 250ms before starting to wire up again CallStackDepth = 20, // Maximum call stack depth before bailing out diff --git a/packages/clarity-js/types/diagnostic.d.ts b/packages/clarity-js/types/diagnostic.d.ts index 3b8ef264..5678e480 100644 --- a/packages/clarity-js/types/diagnostic.d.ts +++ b/packages/clarity-js/types/diagnostic.d.ts @@ -9,12 +9,6 @@ export interface ScriptErrorData { stack: string; } -export interface ImageErrorData { - source: string; - target: Target; - region?: number; -} - export interface LogData { code: Code; name: string; diff --git a/packages/clarity-js/types/interaction.d.ts b/packages/clarity-js/types/interaction.d.ts index 817a2922..046f3898 100644 --- a/packages/clarity-js/types/interaction.d.ts +++ b/packages/clarity-js/types/interaction.d.ts @@ -17,6 +17,12 @@ export const enum Setting { TimelineSpan = 2 * Time.Second, // 2 seconds } +export const enum Clipboard { + Cut = 0, + Copy = 1, + Paste = 2 +} + /* Helper Interfaces */ export interface PointerState { time: number; @@ -30,12 +36,24 @@ export interface ClickState { data: ClickData; } +export interface ClipboardState { + time: number; + event: number; + data: ClipboardData; +} + export interface ScrollState { time: number; event: number; data: ScrollData; } +export interface SubmitState { + time: number; + event: number; + data: SubmitData; +} + export interface InputState { time: number; event: number; @@ -63,6 +81,10 @@ export interface InputData { value: string; } +export interface SubmitData { + target: Target; +} + export interface PointerData { target: Target; x: number; @@ -83,6 +105,11 @@ export interface ClickData { hash: string; } +export interface ClipboardData { + target: Target; + action: Clipboard; +} + export interface ResizeData { width: number; height: number; diff --git a/packages/clarity-visualize/package.json b/packages/clarity-visualize/package.json index 6fa09ff9..1c8d22d1 100644 --- a/packages/clarity-visualize/package.json +++ b/packages/clarity-visualize/package.json @@ -1,6 +1,6 @@ { "name": "clarity-visualize", - "version": "0.6.26", + "version": "0.6.27", "description": "An analytics library that uses web page interactions to generate aggregated insights", "author": "Microsoft Corp.", "license": "MIT", @@ -27,7 +27,7 @@ "url": "https://github.com/Microsoft/clarity/issues" }, "dependencies": { - "clarity-decode": "^0.6.26" + "clarity-decode": "^0.6.27" }, "devDependencies": { "@rollup/plugin-commonjs": "^19.0.1",