Skip to content

Support inline events in the main v12 script - #106

Merged
adriaandotcom merged 3 commits into
v12from
feat/inline-events
Oct 15, 2026
Merged

adriaandotcom merged 3 commits into
v12from
feat/inline-events

Conversation

@simple-analytics-ai

@simple-analytics-ai simple-analytics-ai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Summary

Closes #105

Add the documented inline event features to the main tracker (src/default.js) while keeping the implementation small and preserving website behaviour:

  • Delegate tagged clicks and form submissions from document, so dynamically inserted elements and current data-simple-event-* metadata are picked up without observers or polling. Every event carries a tag field; form events also carry the submit button label. Form field values are never read.
  • Take the submit button label only from the browser-provided event.submitter, preferring textContent and falling back to value for <input type="submit">. Internal whitespace is collapsed to single spaces before trimming and capping at 200 characters, so an icon/span button keeps a stable button_text when its markup is reindented.
  • Send inline events with a keepalive fetch GET (mode: "no-cors", credentials: "omit") so they survive navigation, falling back to the existing image pixel when fetch is missing, throws, or rejects. Native navigation, form validation, formaction overrides and SPA handlers stay in control.
  • The old beta inline-events.js helper is not detected or supported. Remove that extra script when using the built-in handlers, otherwise events are counted twice.
  • Add optional confirmed 404 events via data-detect-404="true" or sa_settings.detect404 = true. The tracker scans the page title and body text for "404", confirms with a HEAD request, and only then sends a page_404 event with the query string and fragment stripped from the URL metadata. Existing installations send no extra requests by default.
  • Compile the handlers only into builds that support events, so light.js is unaffected apart from the shared transport refactor.
  • Add test/unit/inline-events.test.js plus a test:unit script, and run the full unit suite in the BrowserStack workflow after pnpm run build.
  • Bump the handlebars build dependency from 4.7.6 to 4.7.10 and add jsdom 26.1.0 for the unit tests.

Changes:

  • Support inline event attributes in the main v12 tracker - Requested: integrate documented inline events without overengineering, keep the script small and website behavior intact, and open a separate PR based on and targeting v12.
  • Simplify inline events after review - Requested: remove compatibility checks for the beta helper, measure repeated attribute strings, clarify the DOM alias, and shorten submit-button metadata extraction.
  • Normalize inline submit button label whitespace - Requested: address scripts PR Support inline events in the main v12 script #106 review comment 4282258759 so formatting a button across multiple lines does not fragment button_text metadata.

Security implications

Has security impact - described as: the production tracker starts collecting new end-user interaction data on every install that supports events — event names, element tag, data-simple-event-* attribute values and submit-button text — all of which are opt-in per element through HTML attributes, and form field values are explicitly excluded. Opt-in 404 detection additionally issues a HEAD request to the customer's own origin for the current URL; the resulting page_404 metadata strips query strings and fragments. Inline events are sent with credentials: "omit", and the handlebars build dependency is upgraded to a patched release.

Testing

Run by Claude on this branch (f1b1428) with Node 22.23.3:

  • npm install — passed.
  • npm run build — passed; the only regenerated-output difference versus the committed dist/ is the build date stamp and local minifier output in the untouched auto-events.js bundle, so the committed dist/latest/latest.dev.js is in sync with src/.
  • npm run test:unit — 33 passing, 0 failing, including the new submit-label whitespace test.

Reported by the author and not re-run here: local Chromium navigation checks, formatting, and bundle-size measurements.

Checklist

  • Linked to an issue
  • Tested
  • Asked for a review

Requested: integrate documented inline events without overengineering, keep the script small and website behavior intact, and open a separate PR based on and targeting v12.

Use delegated click/submit handlers and keepalive GETs with the existing image fallback. Preserve installed inline helpers and make confirmed 404 detection opt-in. Add focused tests and patch the vulnerable Handlebars build dependency.
Comment thread src/default.js Outdated
button.form === element &&
(button.tagName === "BUTTON" ||
(button.tagName === "INPUT" && button.type === "submit"))
? button.value || button.textContent || ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

button.value shadows the label, and the text is neither trimmed nor capped: <button name="action" value="save">Save changes</button> reports button_text: "save", <button>\n Join now\n</button> reports "\n Join now\n ", and a long button pushes the simple.gif URL past 5 KB (verified against dist/latest/latest.dev.js).

Suggested change
? button.value || button.textContent || ""
? (button.textContent || button.value || "")
.replace(/\s+/g, " ")
.trim()
.slice(0, 200)

With this, <input type="submit" value="Join"> still falls through to value, and all 15 inline-event tests still pass.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 7f712bd with the shorter submitter-only path: prefer textContent, fall back to value for submit inputs, trim outer whitespace and cap the result at 200 characters. Added checks for a label differing from the button value, oversized labels, submit inputs, and missing submitters. All 32 unit tests pass.

Comment thread src/default.js Outdated
var detect404 = function () {
if (
hasInlineHelper() ||
!/[^0-9.,]404[^0-9.,]/.test(documentElement.innerHTML)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Matching 404 against the whole serialized DOM false-positives constantly, so opted-in sites send an extra HEAD request for their own URL on ordinary 200 pages — I confirmed a request fires for <div style="width: 404px">, <script src="/assets/main-404abc.js">, <div class="w-404"> and <div data-id="404">. Narrow the scan to rendered text (e.g. doc.title plus doc.body.textContent) so the added request matches the "existing installs must opt in" cost the comment above promises.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Changed the probe to title plus body text in 7f712bd. A test containing all four attribute-only examples now confirms that no HEAD request is issued. Confirmed 404 events still work before and after DOM ready in Chromium, and the response status must still be 404.

Comment thread .github/workflows/browserstack.yml Outdated
run: pnpm run test:matrix

- name: Test inline events
run: pnpm run build && pnpm exec mocha test/unit/inline-events.test.js --exit

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This runs only the new file, so the seven other test/unit/*.test.js suites that cover the same src/default.js this PR rewrites still never run in CI. Widening the glob picks them up (I ran all 31 locally against this branch and they pass):

Suggested change
run: pnpm run build && pnpm exec mocha test/unit/inline-events.test.js --exit
- name: Run unit tests
run: pnpm run build && pnpm exec mocha "test/unit/*.test.js" --exit

Worth also adding a test:unit script to package.json — AGENTS.md tells contributors to run npm run test:unit, which does not exist.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added test:unit and changed this CI step to run the complete test/unit/*.test.js suite in 7f712bd. All 32 tests pass locally, including the 16 inline-event tests.

@github-actions

Copy link
Copy Markdown

Documentation update

_docs/55_events/00_inline-events.md

Location: Replace the whole page body (keep the front matter, but update last_modified_at), because the separate helper script is no longer required and the click/submit behaviour changed.

Copy and paste this into the documentation repository:

---
title: Inline events
category: events
permalink: /events/inline
hidden: true
last_modified_at: 2026-10-15
---

The tracker watches your page for any element with a `data-simple-event` attribute and automatically sends events to Simple Analytics when the user interacts with it. You don’t need to write any extra JavaScript calls—just add the right attributes to your HTML.

<blockquote class="red">
  <p>Warning, this feature is in beta and can change any time.</p>
</blockquote>

### Installation

Inline events are built into the tracker, so the regular snippet is all you need.

```html
<script async src="https://scripts.simpleanalyticscdn.com/latest.js"></script>

If you still load the separate inline-events.js helper, the tracker steps aside and lets the helper handle everything, so you never get duplicate events. You can remove that extra script whenever you want—the built-in handlers take over automatically.

Inline events are not included in light.js, which has no event support.

Tracking button clicks

To track a click, add data-simple-event="event_name" to any clickable element. The tracker sends the event in the background and never blocks the click, so links, downloads, target="_blank", keyboard-modified clicks and your own click handlers keep working exactly as before.

<a href="/pricing" data-simple-event="visit_pricing_page">See pricing</a>

Tracking form submissions

Put data-simple-event="form_name" on your form tag. When the user submits, the tracker sends an event with the name of the form and the text of the submit button, and then lets the browser submit the form as usual. Form field values are never collected.

<form action="/signup" data-simple-event="signup_form">
  <input name="email" type="email" />
  <input name="password" type="password" />
  <button type="submit">Sign up</button>
</form>

Adding custom metadata

Any attribute starting with data-simple-event- is included as event metadata. Dashes convert to underscores. Every event also includes a tag field with the lowercase tag name of the element.

<button
  data-simple-event="select_plan"
  data-simple-event-plan="enterprise"
  data-simple-event-referrer="homepage"
>
  Choose Enterprise
</button>

Manual event tracking

You can also fire events from code using sa_event("event_name", { key: value }). This works before or after the automatic binding.

// custom event on newsletter popup
sa_event("newsletter_signup", { source: "modal" })

404 detection

404 detection is off by default because it adds a request to your server. Turn it on with the data-detect-404 attribute:

<script
  async
  data-detect-404="true"
  src="https://scripts.simpleanalyticscdn.com/latest.js"
></script>

Or with the settings object:

<script>
  window.sa_settings = { detect404: true }
</script>

When enabled, the tracker checks for "404" in your page HTML once the DOM is ready. If it finds a match and a HEAD request confirms the page really returns status 404, it sends a page_404 event with the URL and status code. Query strings and fragments are stripped from the URL metadata.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Claude review checkpoint

Reviewed commit f1b14281d7b8d5054c6874e8b59e61393eb2c476. This is used to keep later automated reviews focused on changes Claude has not checked yet.

@github-actions github-actions Bot added the change: needs review Changes affecting security, data protection, or system stability. label Oct 15, 2026
@github-actions

Copy link
Copy Markdown

SDLC label: change: needs review because the production tracker begins collecting new end-user interaction data by default on every existing install — event names, element tag, data-simple-event-* attribute values and submit-button text — and adds an opt-in 404 check that issues HEAD requests to customer origins, materially changing how visitor data is collected

Comment thread src/default.js Outdated
Comment on lines +1142 to +1144
var hasInlineHelper = function () {
return doc.querySelector('script[src*="inline-events.js"]');
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This was just a test helper, fine if this collides, it was beta anyway. Remove this check.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed the beta-helper detection from both inline-event handling and 404 detection in 7f712bd. The built-in handlers now run even if the old helper tag is present; the updated PR description says to remove that beta script to avoid duplicate events.

Comment thread src/default.js
Comment thread src/default.js Outdated
var detect404 = function () {
if (
hasInlineHelper() ||
!/[^0-9.,]404[^0-9.,]/.test(documentElement.innerHTML)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we cna't use a abbrivations of documentElement.innerHTML, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The existing documentElement alias is safe: it holds the DOM object, and reading its .innerHTML later reads the current HTML. Caching the HTML string itself would be stale. In 7f712bd the 404 check instead reads the title and body text, which also prevents attributes such as width: 404px from triggering a HEAD request. The focused attribute test and before/after-DOM-ready Chromium checks pass.

Comment thread src/default.js Outdated
Comment on lines +1171 to +1177
metadata.button_text =
button &&
button.form === element &&
(button.tagName === "BUTTON" ||
(button.tagName === "INPUT" && button.type === "submit"))
? button.value || button.textContent || ""
: "";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can we make this less perfect, but a lot shorter?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Shortened this in 7f712bd: use event.submitter directly and remove the active-element fallback plus the tag/form checks. If the browser provides no submitter, button text is simply empty and the form event is still recorded. The label is preferred over value, trimmed and capped at 200 characters. Form validation, native submitter overrides, and SPA submissions pass the browser checks.

Requested: remove compatibility checks for the beta helper, measure repeated attribute strings, clarify the DOM alias, and shorten submit-button metadata extraction.

Keep literals because sharing them increases gzip size. Prefer trimmed, capped submitter labels and scan title/body text for 404s. Address the related bot feedback by running the complete unit suite in CI.
Comment thread src/default.js Outdated
if (isForm) {
var button = event.submitter;
metadata.button_text = button
? (button.textContent || button.value || "").trim().slice(0, 200)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

trim() only strips the ends, so the very common icon/span submit button keeps its markup indentation — I verified <button>\n <svg></svg>\n <span>Save</span>\n <span>changes</span>\n</button> sends button_text: "Save\n changes", and the same button reformatted produces a different value, fragmenting the metadata. Collapse internal whitespace too:

Suggested change
? (button.textContent || button.value || "").trim().slice(0, 200)
? (button.textContent || button.value || "")
.replace(/\s+/g, " ")
.trim()
.slice(0, 200)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

implement this

@simple-analytics-ai simple-analytics-ai Bot Oct 15, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in f1b1428. Submit-button labels now collapse internal whitespace before the existing trim and 200-character limit, so compact and indented markup both record Save changes.

Local verification: the new regression fails against the previous compiled tracker; all 33 unit tests, the build, formatting, and diff checks pass after the fix. Remote BrowserStack CI and automated review both passed on this exact commit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Confirmed implemented in the current head, f1b1428: .replace(/\s+/g, " ") runs before trimming and the 200-character cap. The regression verifies that compact and indented icon/span button markup both produce Save changes. All 33 unit tests pass locally, and the remote unit-test step has also passed. This costs 20 bytes raw / 8 bytes gzip.

Comment thread src/default.js
@github-actions

Copy link
Copy Markdown

Documentation update

_docs/55_events/00_inline-events.md

Location: Two corrections to the page text proposed earlier in this PR: replace the inline-events.js helper paragraph at the end of ### Installation, and replace the final paragraph of ### 404 detection. The tracker no longer detects the helper (so both would fire), and 404 detection now scans the title and body text instead of the page HTML.

Copy and paste this into the documentation repository:

<!-- End of "### Installation", replacing the paragraph about the helper -->

The tracker does not detect the old `inline-events.js` helper. If you still load that script, remove it — otherwise every tagged click and submit is counted twice.

Inline events are not included in `light.js`, which has no event support.

<!-- End of "### 404 detection", replacing the final paragraph -->

When enabled, the tracker checks the page title and the visible body text for "404" once the DOM is ready. If it finds a match and a `HEAD` request confirms the page really returns status 404, it sends a `page_404` event with the URL and status code. Query strings and fragments are stripped from the URL metadata.

Requested: address scripts PR #106 review comment 4282258759 so formatting a button across multiple lines does not fragment button_text metadata.

Collapse whitespace before trimming and applying the existing 200-character limit. Add a regression for equivalent compact and indented button markup, and regenerate the affected tracker bundles.

Verification: regression fails on the previous compiled tracker; all 33 unit tests pass after the fix. Build, formatting, and diff checks pass.
@simple-analytics-ai

Copy link
Copy Markdown
Author

In response to #106 (comment) and #106 (comment):

Prepared a patch against current docs/master for the final v12 behavior: built-in inline events, removal of the beta helper, native click/form handling, normalized submit-button labels, and opt-in 404 detection using title/body text.

The patch applies cleanly. There is no matching open docs PR to update. I have requested owner approval for a supporting docs PR because this automation is authorized to push to existing PR branches. Publication of these instructions must follow the relevant script PR and the v12 release; the patch remains local pending that approval.

@adriaandotcom
adriaandotcom merged commit 38248a0 into v12 Oct 15, 2026
20 checks passed
@adriaandotcom
adriaandotcom deleted the feat/inline-events branch October 15, 2026 13:16
@github-actions github-actions Bot mentioned this pull request Oct 15, 2026
3 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change: needs review Changes affecting security, data protection, or system stability.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant