Skip to content

Commit

Permalink
Instrument history api (push/popstate) (#140)
Browse files Browse the repository at this point in the history
* Add pushState tracking + test

* Preserve pushState arity

* Add popstate handler, tests

* Instrument history first time we track a page view
  • Loading branch information
benvinegar authored Jan 7, 2025
1 parent be18e7f commit 17c6b53
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 2 deletions.
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"build": "remix vite:build",
"preview": "wrangler pages dev ./build/client",
"deploy": "wrangler pages deploy ./build/client",
"lint": "eslint --ignore-path ../../.gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"lint": "eslint --ignore-path ../../.gitignore --ignore-pattern public/tracker.js --cache --cache-location ./node_modules/.cache/eslint .",
"test": "TZ=EST vitest run",
"test-ci": "TZ=EST vitest run --coverage",
"typecheck": "tsc",
Expand Down
19 changes: 19 additions & 0 deletions packages/tracker/integration/03_pushState/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<title>03 PushState</title>
</head>
<body>
<button
onClick="history.pushState({page: 2}, '', '/03_pushState/part_2')"
>
Part 2
</button>
<script
id="counterscale-script"
data-site-id="your-unique-site-id"
src="http://localhost:3004/tracker.js"
defer
></script>
</body>
</html>
66 changes: 66 additions & 0 deletions packages/tracker/integration/03_pushState/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { test, expect, Request } from "@playwright/test";

test.describe("03_pushState", () => {
test("tracks pushState and popState events as pageviews", async ({
page,
}) => {
// Listen for all console events and handle errors
page.on("console", (msg) => {
if (msg.type() === "error")
console.log(`Error text: "${msg.text()}"`);
});

const collectRequests: Request[] = [];
page.on("request", (request) => {
if (request.url().includes("/collect")) {
collectRequests.push(request);
}
});

await page.goto("http://localhost:3004/03_pushState/");
expect(collectRequests).toHaveLength(1);

let request = collectRequests[0];
expect(request).toBeTruthy();
expect(request.method()).toBe("GET");
let params = new URLSearchParams(request.url().split("?")[1]);
expect(params.get("sid")).toBe("your-unique-site-id");
expect(params.get("h")).toBe("http://localhost"); // drops port
expect(params.get("p")).toBe("/03_pushState/");
expect(params.get("r")).toBe("");

// click <button> to initiate pushState
await page.click("button");

// assert url changed
expect(page.url()).toBe("http://localhost:3004/03_pushState/part_2");

expect(collectRequests).toHaveLength(2);
request = collectRequests[1];
expect(request).toBeTruthy();
expect(request.method()).toBe("GET");
params = new URLSearchParams(request.url().split("?")[1]);
expect(params.get("sid")).toBe("your-unique-site-id");
expect(params.get("h")).toBe("http://localhost"); // drops port
expect(params.get("p")).toBe("/03_pushState/part_2");
expect(params.get("r")).toBe("");

// back button (popState)
await page.goBack({
// NOTE: "load" already fired because the page is not being reloaded, so need
// to wait on networkidle instead
waitUntil: "networkidle",
});

expect(page.url()).toBe("http://localhost:3004/03_pushState/");
expect(collectRequests).toHaveLength(3);
request = collectRequests[2];
expect(request).toBeTruthy();
expect(request.method()).toBe("GET");
params = new URLSearchParams(request.url().split("?")[1]);
expect(params.get("sid")).toBe("your-unique-site-id");
expect(params.get("h")).toBe("http://localhost"); // drops port
expect(params.get("p")).toBe("/03_pushState/");
expect(params.get("r")).toBe("");
});
});
36 changes: 35 additions & 1 deletion packages/tracker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ SOFTWARE.

const queue = (window.counterscale && window.counterscale.q) || [];

const context = {
isInstrumented: false,
};

type ConfigType = {
siteId?: string;
reporterUrl?: string;
Expand Down Expand Up @@ -77,9 +81,38 @@ function findReporterScript() {
return el;
}

function trackPageview(vars: { [key: string]: string }) {
function instrumentHistoryBuiltIns() {
if (context.isInstrumented) {
return false;
}

context.isInstrumented = true;
const origPushState = history.pushState;

// NOTE: Intentionally only declaring 2 parameters for this pushState wrapper,
// because that is the arity of the built-in function we're overwriting.

// See: https://blog.sentry.io/wrap-javascript-functions/#preserve-arity

// eslint-disable-next-line
history.pushState = function (data, title /*, url */) {
// eslint-disable-next-line
origPushState.apply(this, arguments as any);
trackPageview();
};

addEventListener("popstate", () => {
trackPageview();
});

// TODO: Should offer some way to clean this up
}

function trackPageview(vars?: { [key: string]: string }) {
vars = vars || {};

instrumentHistoryBuiltIns();

// ignore prerendered pages
if (
"visibilityState" in document &&
Expand Down Expand Up @@ -201,6 +234,7 @@ queue.forEach(function (cmd: Command) {
const siteId = script.getAttribute("data-site-id");
if (siteId) {
set("siteId", siteId);

trackPageview({});
}
})();

0 comments on commit 17c6b53

Please sign in to comment.