babel-plugin-analytics-hash
v0.1.0
Published
Auto-generate stable, structural analytics IDs for JSX elements at build time
Maintainers
Readme
babel-plugin-analytics-hash
Auto-generate stable, unique data-analytics-id attributes on every JSX element at build time. Configure additional tracking attributes via a JSON file — no developer involvement needed.
The problem
Every time your analytics team needs a data-track attribute on a UI element, it becomes a developer ticket → PR → review → deploy cycle. That takes days to weeks for what's essentially a config change.
Existing solutions either require manual developer discipline (data-testid conventions), rely on fragile CSS selectors (GTM), or are expensive auto-capture platforms (Heap, FullStory).
How this works
- Babel plugin walks the JSX AST at build time and injects a
data-analytics-idon every element - Each ID is a deterministic SHA-256 hash of the element's structural identity — file path, component ancestry, element type, stable props, sibling index
- A manifest file maps hashes to human-readable paths (e.g.
e7ddfb4e → src/pages/Login.jsx > LoginPage > button[type=submit]) - An analytics config file maps hashes to additional attributes — the plugin injects these at build time
The analytics team inspects an element on staging, grabs the hash, adds their tracking config to a JSON file, and the next build picks it up. No code changes, no PRs.
What makes the hash stable
Hashes are derived from AST structure, not source positions. They're insensitive to:
- Whitespace and formatting changes
- Comment additions/removals
- Import reordering
- Variable renames
- Unrelated code changes in the same file
They change when the element's actual identity changes: different component, different type, different file, changed stable props.
How this differs from data-testid plugins
Existing plugins like babel-plugin-react-data-testid generate IDs from component names (LoginForm_button). These aren't unique across siblings, break on component renames, and don't include any workflow for non-developers to configure tracking. This plugin produces unique structural hashes with a config-driven attribute injection layer.
Install
npm install -D babel-plugin-analytics-hashPeer dependency: @babel/core ^7.0.0
Setup
Vite
// vite.config.js
import react from '@vitejs/plugin-react'
export default {
plugins: [
react({
babel: {
plugins: [
['babel-plugin-analytics-hash', {
configPath: './analytics-tags.json',
manifestPath: './analytics-manifest.json',
hashLength: 8,
}]
]
}
})
]
}Webpack / babel-loader
// webpack.config.js or babel.config.json
{
"plugins": [
["babel-plugin-analytics-hash", {
"configPath": "./analytics-tags.json",
"manifestPath": "./analytics-manifest.json"
}]
]
}Next.js
// babel.config.json
{
"presets": ["next/babel"],
"plugins": [
["babel-plugin-analytics-hash", {
"configPath": "./analytics-tags.json",
"manifestPath": "./analytics-manifest.json"
}]
]
}Create React App (via CRACO)
// craco.config.js
module.exports = {
babel: {
plugins: [
['babel-plugin-analytics-hash', {
configPath: './analytics-tags.json',
manifestPath: './analytics-manifest.json',
}]
]
}
}Rollup
import babel from '@rollup/plugin-babel'
export default {
plugins: [
babel({
babelHelpers: 'bundled',
plugins: [['babel-plugin-analytics-hash']]
})
]
}It's a standard Babel plugin — if your toolchain runs Babel on JSX, it'll work.
Usage
Build output
Every element gets a hash attribute:
<!-- input -->
<button type="submit" className="btn-primary">Sign In</button>
<!-- output -->
<button data-analytics-id="e7ddfb4e" type="submit" className="btn-primary">Sign In</button>Manifest
Generated on each build at manifestPath:
{
"_totalElements": 247,
"elements": {
"e7ddfb4e": "src/pages/Login.jsx > LoginPage > form > button[type=submit].btn-primary",
"776e96df": "src/pages/Login.jsx > LoginPage > form > input#email[name=email][type=email]",
"821817cf": "src/pages/Dashboard.jsx > Dashboard > div#promo-slot.personalization-slot"
}
}Use this to look up what a hash corresponds to, or search by keyword to find elements.
Analytics config
Create analytics-tags.json in your project root:
{
"e7ddfb4e": {
"data-track-click": "login_submit",
"data-track-category": "authentication"
},
"821817cf": {
"data-personalization": "adobe-target-promo-banner"
}
}On the next build, the plugin reads this and injects those attributes alongside the hash:
<button data-analytics-id="e7ddfb4e" data-track-click="login_submit" data-track-category="authentication" ...>Works with any tracking tool — Adobe Analytics, GTM event listeners, Segment, Mixpanel, Adobe Target personalization containers, or custom scripts that query data-* attributes.
Stale config warnings
If the config references a hash that no longer matches any element (deleted or restructured), the build logs a warning:
[analytics-hash] Warning: hash "e7ddfb4e" configured but no matching element foundWorkflow summary
- Install plugin, add to Babel config
- Build → every element gets a
data-analytics-id - Analytics person inspects element on staging → copies hash
- Adds entry to
analytics-tags.json→ commits - Next build auto-injects configured attributes
- No developer ticket needed
Options
All optional. Works with zero config.
{
attributeName: "data-analytics-id", // injected attribute name
hashLength: 10, // hex chars (8 ≈ 65K safe, 10 ≈ 1M safe)
configPath: "./analytics-tags.json",
manifestPath: "./analytics-manifest.json",
// Element filtering
ignoreElements: ["html", "head", "meta", "script", "style", "br", "hr", "Fragment", ...],
onlyElements: [], // e.g. ["button", "input", "a"] to limit scope
excludeFiles: ["node_modules", "\\.test\\.", "\\.spec\\."],
includeFiles: [], // e.g. ["src/pages/", "src/features/"]
excludeComponents: [], // e.g. ["Icon", "Spinner"]
onlyInsideComponents: [], // e.g. ["App"]
skipIfHasAttribute: "", // e.g. "data-testid" to skip pre-tagged elements
enabled: true, // set false to disable (e.g. in CI)
}Performance
Build time: One AST pass + one SHA-256 per element. ~1–3s for a 10K-element app. Per-file during HMR, so effectively invisible in dev.
DOM overhead: ~28 bytes per element for the attribute. After gzip, ~22 KB total for a 10K-element app. Smaller than a favicon. Facebook ships data-testid to billions of pageviews — this is a non-concern.
Manifest: ~100 bytes/element, never shipped to the browser. 10K elements ≈ 1 MB file in your project dir.
Production safety
data-* attributes are part of the HTML spec. They don't execute code, don't expose internals (hashes are opaque), and don't create security vulnerabilities. Inspect Amazon, Netflix, or Airbnb — custom data attributes are everywhere in production.
If you'd rather strip them in prod, set enabled: false in your production Babel config.
Collision handling
With 8 hex chars (32 bits), collision probability stays under 1% up to ~65K elements. With 10 chars (40 bits), you're safe past 1M. The plugin also runs collision detection at build time and logs a warning if one occurs. Bump hashLength if you ever see one.
Roadmap
- [ ] Chrome extension for visual hash discovery
- [ ] SWC plugin (for non-Babel setups)
- [ ] CLI for manifest diffs between releases
- [ ] Dashboard UI for manifest browsing + config editing
Contributing
See CONTRIBUTING.md.
License
MIT
