Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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",
"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",
"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",
"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"
},
"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",
"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",
"clarity-js": "^0.8.70",
"clarity-visualize": "^0.8.70"
},
"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",
"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",
"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";
export default version;
34 changes: 34 additions & 0 deletions packages/clarity-js/src/layout/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { AgenticBrowserSignal, Constant as DataConstant } from "@clarity-types/data";
import { Constant } from "@clarity-types/layout";
import { set } from "@src/data/variable";

let seen: boolean[] = [];

export function start(): void {
seen = [];
}

export function detect(attributes: { [key: string]: string }): void {
let signal = identify(attributes[Constant.Id]);
if (signal && !seen[signal]) {
seen[signal] = true;
set(DataConstant.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;
}
}
2 changes: 2 additions & 0 deletions packages/clarity-js/src/layout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { start as regStart, stop as regStop } from "@src/layout/region";
import { start as styStart, stop as styStop } from "@src/layout/style";
import { start as animStart, stop as animStop } from "@src/layout/animation";
import { start as custStart, stop as custStop } from "@src/layout/custom";
import { start as agentStart } from "@src/layout/agent";
import { bind } from "@src/core/event";
import config from "@src/core/config";

export { hashText } from "@src/layout/dom";

export function start(): void {
// The order below is important and is determined by interdependencies of modules
agentStart();
docStart();
regStart();
domStart();
Expand Down
2 changes: 2 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,7 @@ export default function (node: Node, source: Source, timestamp: number): Node {
let element = (node as HTMLElement);
let tag = element.tagName;
let attributes = getAttributes(element);
agent.detect(attributes);
// 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
10 changes: 10 additions & 0 deletions packages/clarity-js/types/data.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,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 Expand Up @@ -346,6 +355,7 @@ export const enum Constant {
UserId = "userId",
SessionId = "sessionId",
PageId = "pageId",
AgenticBrowserSignal = "<!>ABS",
Mask = "•", // Placeholder character for explicitly masked content
Digit = "▫", // Placeholder character for digits
Letter = "▪", // Placeholder character for letters
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",
"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"
},
"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 Variable = "<!>ABS";
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); }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This callback runs inside an injected HTML <script> as plain JavaScript, so a TypeScript annotation would be invalid here. This matches existing test patterns; no change needed.

});
</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.variable || []) {
if (event.data && event.data[Variable]) {
signals.push(...event.data[Variable]);
}
}
}
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 variable for similar ids", async ({ page }): Promise<void> => {
await start(page, `<div id="claude-agent-stop-container-copy"></div>`);

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