Skip to content
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
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"packages": [
"packages/*"
],
"version": "0.8.69",
"version": "0.8.70-beta",
"npmClient": "yarn"
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "clarity",
"private": true,
"version": "0.8.69",
"version": "0.8.70-beta",
"repository": "git+https://github.com/microsoft/clarity.git",
"author": "Sarvesh Nagpal <sarveshn@microsoft.com>",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions packages/clarity-decode/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clarity-decode",
"version": "0.8.69",
"version": "0.8.70-beta",
"description": "An analytics library that uses web page interactions to generate aggregated insights",
"author": "Microsoft Corp.",
"license": "MIT",
Expand All @@ -26,7 +26,7 @@
"url": "https://github.com/Microsoft/clarity/issues"
},
"dependencies": {
"clarity-js": "^0.8.69"
"clarity-js": "^0.8.70-beta"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/clarity-devtools/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clarity-devtools",
"version": "0.8.69",
"version": "0.8.70-beta",
"private": true,
"description": "Adds Clarity debugging support to browser devtools",
"author": "Microsoft Corp.",
Expand All @@ -24,9 +24,9 @@
"url": "https://github.com/Microsoft/clarity/issues"
},
"dependencies": {
"clarity-decode": "^0.8.69",
"clarity-js": "^0.8.69",
"clarity-visualize": "^0.8.69"
"clarity-decode": "^0.8.70-beta",
"clarity-js": "^0.8.70-beta",
"clarity-visualize": "^0.8.70-beta"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/clarity-devtools/static/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "Microsoft Clarity Developer Tools",
"description": "Clarity helps you understand how users are interacting with your website.",
"version": "0.8.69",
"version_name": "0.8.69",
"version": "0.8.70",
"version_name": "0.8.70-beta",
"minimum_chrome_version": "88",
"devtools_page": "devtools.html",
"icons": {
Expand Down
2 changes: 1 addition & 1 deletion packages/clarity-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clarity-js",
"version": "0.8.69",
"version": "0.8.70-beta",
"description": "An analytics library that uses web page interactions to generate aggregated insights",
"author": "Microsoft Corp.",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/clarity-js/src/core/version.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
let version = "0.8.69";
let version = "0.8.70-beta";
export default version;
24 changes: 24 additions & 0 deletions packages/clarity-js/src/layout/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { AgenticBrowserSignal, Dimension } from "@clarity-types/data";
import * as dimension from "@src/data/dimension";

export function detect(id: string): void {
let signal = identify(id);
if (signal) { dimension.log(Dimension.AgenticBrowserSignal, signal.toString()); }
}

function identify(id: string): AgenticBrowserSignal {
switch (id) {
case "claude-agent-glow-border":
return AgenticBrowserSignal.ClaudeAgentGlowBorder;
case "claude-agent-glow-border-inner":
return AgenticBrowserSignal.ClaudeAgentGlowBorderInner;
case "claude-agent-stop-container":
return AgenticBrowserSignal.ClaudeAgentStopContainer;
case "claude-agent-stop-button":
return AgenticBrowserSignal.ClaudeAgentStopButton;
case "claude-phantom-cursor":
return AgenticBrowserSignal.ClaudePhantomCursor;
default:
return AgenticBrowserSignal.None;
}
}
3 changes: 3 additions & 0 deletions packages/clarity-js/src/layout/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as interaction from "@src/interaction";
import * as mutation from "@src/layout/mutation";
import * as schema from "@src/layout/schema";
import * as custom from "@src/layout/custom";
import * as agent from "@src/layout/agent";
import { checkDocumentStyles } from "@src/layout/style";
import { electron } from "@src/data/metadata";

Expand Down Expand Up @@ -94,6 +95,8 @@ export default function (node: Node, source: Source, timestamp: number): Node {
let element = (node as HTMLElement);
let tag = element.tagName;
let attributes = getAttributes(element);
let id = attributes[Constant.Id];
if (id) { agent.detect(id); }
// In some cases, external libraries like vue-fragment, can modify parentNode property to not be in sync with the DOM
// For correctness, we first look at parentElement and if it not present then fall back to using parentNode
parent = node.parentElement ? node.parentElement : (node.parentNode ? node.parentNode as HTMLElement : null);
Expand Down
12 changes: 11 additions & 1 deletion packages/clarity-js/types/data.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ export const enum Dimension {
TimezoneOffset = 35,
Consent = 36,
InteractionNextPaint = 37,
GlobalPrivacyControl = 38
GlobalPrivacyControl = 38,
AgenticBrowserSignal = 39
// 200-300 reserved for internal use
}

Expand Down Expand Up @@ -233,6 +234,15 @@ export const enum BooleanFlag {
True = 1
}

export const enum AgenticBrowserSignal {
None = 0,
ClaudeAgentGlowBorder = 1,
ClaudeAgentGlowBorderInner = 2,
ClaudeAgentStopContainer = 3,
ClaudeAgentStopButton = 4,
ClaudePhantomCursor = 5
}

export const enum GCMConsent {
Unknown = 0,
Granted = 1,
Expand Down
4 changes: 2 additions & 2 deletions packages/clarity-visualize/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clarity-visualize",
"version": "0.8.69",
"version": "0.8.70-beta",
"description": "An analytics library that uses web page interactions to generate aggregated insights",
"author": "Microsoft Corp.",
"license": "MIT",
Expand All @@ -27,7 +27,7 @@
"url": "https://github.com/Microsoft/clarity/issues"
},
"dependencies": {
"clarity-decode": "^0.8.69"
"clarity-decode": "^0.8.70-beta"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.0",
Expand Down
154 changes: 154 additions & 0 deletions test/agentic-browser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { expect, test } from "@playwright/test";
import { decode } from "clarity-decode";
import { readFileSync } from "fs";
import { resolve } from "path";
import { pathToFileURL } from "url";
import type { Page } from "@playwright/test";
import type { Data } from "clarity-decode";

declare global {
interface Window {
clarity: (method: string, ...args: any[]) => void;
payloads: string[];
}
}

const AgenticBrowserSignalDimension = 39;
const Signals = {
ClaudeAgentGlowBorder: "1",
ClaudeAgentGlowBorderInner: "2",
ClaudeAgentStopContainer: "3",
ClaudeAgentStopButton: "4",
ClaudePhantomCursor: "5",
} as const;

async function start(page: Page, markup: string = ""): Promise<void> {
const htmlPath = resolve(__dirname, "./html/core.html");
const htmlFileUrl = pathToFileURL(htmlPath).toString();
const html = readFileSync(htmlPath, "utf8");
const clarity = readFileSync(resolve(__dirname, "../packages/clarity-js/build/clarity.min.js"), "utf8");
await page.goto(htmlFileUrl);
await page.setContent(html.replace("</body>", `${markup}
<script>
window.payloads = [];
${clarity};
clarity("start", {
delay: 50,
projectId: "test",
upload: (payload) => { window.payloads.push(payload); }
});
</script>
</body>
`));
}

async function collect(page: Page): Promise<string[]> {
await page.waitForTimeout(300);
await page.evaluate((): void => window.clarity("stop"));
await page.waitForFunction("window.payloads.length > 0");
const payloads = await page.evaluate((): string[] => window.payloads);
const signals: string[] = [];
for (const payload of payloads.map((value: string): Data.DecodedPayload => decode(value))) {
for (const event of payload.dimension || []) {
if (event.data && event.data[AgenticBrowserSignalDimension]) {
signals.push(...event.data[AgenticBrowserSignalDimension]);
}
}
}
return signals;
}

test.describe("Agentic browser markers", (): void => {
test("captures the Claude marker family", async ({ page }): Promise<void> => {
await start(page, `
<div id="claude-agent-glow-border"></div>
<div id="claude-agent-glow-border-inner"></div>
<div id="claude-agent-stop-container"></div>
<button id="claude-agent-stop-button"></button>
<div id="claude-phantom-cursor"></div>
`);

expect((await collect(page)).sort()).toEqual(Object.values(Signals).sort());
});

test("captures a marker inserted after load", async ({ page }): Promise<void> => {
await start(page);
await page.evaluate((): void => {
const marker = document.createElement("div");
marker.id = "claude-agent-glow-border";
document.body.appendChild(marker);
});

expect(await collect(page)).toEqual([Signals.ClaudeAgentGlowBorder]);
});

test("captures an id assigned after insertion", async ({ page }): Promise<void> => {
await start(page);
await page.evaluate((): void => {
const marker = document.createElement("div");
marker.id = "pending-marker";
document.body.appendChild(marker);
});
await page.waitForTimeout(150);
await page.evaluate((): void => {
document.getElementById("pending-marker").id = "claude-agent-stop-container";
});

expect(await collect(page)).toEqual([Signals.ClaudeAgentStopContainer]);
});

test("retains a transient marker removed before upload", async ({ page }): Promise<void> => {
await start(page);
await page.evaluate((): void => {
const marker = document.createElement("button");
marker.id = "claude-agent-stop-button";
document.body.appendChild(marker);
marker.remove();
});

expect(await collect(page)).toEqual([Signals.ClaudeAgentStopButton]);
});

test("emits each marker once per page", async ({ page }): Promise<void> => {
await start(page);
await page.evaluate((): void => {
for (let i = 0; i < 2; i++) {
const marker = document.createElement("div");
marker.id = "claude-phantom-cursor";
document.body.appendChild(marker);
}
});

expect(await collect(page)).toEqual([Signals.ClaudePhantomCursor]);
});

test("captures a marker in an open shadow root", async ({ page }): Promise<void> => {
await start(page);
await page.evaluate((): void => {
const host = document.createElement("div");
document.body.appendChild(host);
const marker = document.createElement("div");
marker.id = "claude-phantom-cursor";
host.attachShadow({ mode: "open" }).appendChild(marker);
});

expect(await collect(page)).toEqual([Signals.ClaudePhantomCursor]);
});

test("captures a marker in a same-origin iframe", async ({ page }): Promise<void> => {
await start(page);
await page.evaluate((): void => {
const frame = document.createElement("iframe");
frame.srcdoc = "<div id='claude-agent-stop-button'></div>";
document.body.appendChild(frame);
});

expect(await collect(page)).toEqual([Signals.ClaudeAgentStopButton]);
});

test("does not emit a dimension for similar ids", async ({ page }): Promise<void> => {
await start(page, `<div id="claude-agent-stop-container-copy"></div>`);

expect(await collect(page)).toEqual([]);
});
});
Loading