Q. What do you decide first when designing an event taxonomy?
Business questions, not event names. Once the questions are settled the event list follows, and the real decision after that is whether each value gets sent as a parameter or left out and recovered later through a join. In a sheet covering 46 events, 14 parameters were marked as not sent, and the reasons split five ways.
Three things you can use today
- If you cannot write the question an event answers in one sentence, do not create that event
- Keep a row for parameters you do not send, because each reason has a different recovery path
- Define the reversal event in the same sitting as the forward event it mirrors
The spec had events the screens did not
I once took an event spec that was already live and checked it line by line against the code and the actual screens. Around 40 events, and on paper it looked tidy. By the end of the pass, this is what had surfaced.
- A consultation button click event was defined, but no page on the site rendered that button component at all
- An event named
open_..._modalwas not a modal, it was a route change to a different URL - A "viewed the price summary" event never fired below 1024px, because that component is swapped for a different one on mobile
- Parameter names matched the spec 100%, yet
item_categorywas reading a variable calledcategory, and that variable held a category number rather than a service type
Four separate bugs, apparently. Except they share one cause. The events were defined first and the screens were checked last. Flip the order and none of the four happens.
So this is not a naming rulebook. There are three questions to answer. What do you decide first. How do you fit forty-something events onto one sheet. And the one that takes the most work, how do you split the values you send from the values you do not.
What I lived through and what I looked up. The four symptoms above and the sheet structure that follows come from work I actually ran and audited. The GA4 limits, on the other hand, are numbers I confirmed in Google's official documentation rather than hit myself, and the off-page events in the last section are a design proposal that has not been implemented. I flag which is which as we go.
The headwater is a question, not an event name
Taxonomy sounds like a heavy word, but the meaning is plain. It is the classification scheme that fixes what gets recorded, under which name, with which values. Same idea as a library catalogue.
This is where most teams start with the event list instead. Scan the screens, count anything clickable, attach a plausible name, then fill in the "why do we measure this" column afterwards. That is exactly backwards.
Steps 1 and 2 are written nowhere in the code or the screens. A human has to decide them, which is why they sit upstream.
Writing step 1 in a form that produces an answer is close to the whole game. "Analyse interest levels" is not a question. That is a wish with punctuation on the end. "What share of people who viewed a product detail page request a consultation" is a question. The first version lets you retire nothing, while the second tells you immediately that you need exactly two events.
The industry references land on the same order. Amplitude's tracking plan guide says to start by writing down the pressing questions you have about your users and how they use the product, and it narrows the event list to the handful of core events that explain the activation path, telling you to ignore the rest.
What happens when you skip step 2, the screen mapping, is already at the top of this article. Events appear for UI that does not exist. So if you keep only one audit rule, keep this one. For every event you must be able to point at "this element, on this screen."
If you cannot, it is one of three things. It is an entry or impression event, so having no element is correct. It is a server-side event, so having no element on the web is correct. Or the design is running ahead of the screens. For the first two, write that down. For the third, confirm the UI is actually planned or drop the event.
If you cannot write the question in one sentence, do not create the event.
The row is not one event, it is one event-and-parameter pair
Once the questions and screens are settled, it is time to build the sheet. Get the shape wrong here and everything downstream is painful.
The most common shape gives each event one row and lists its parameters, comma separated, in a single cell. It reads nicely at first. It collapses the moment you pass forty events.
List parameters inside a cell and you lose the place to record that parameter's status, type, and source.
With this structure a 46-event sheet runs to 179 rows. It looks long, but the payoff is real. You can search by parameter name, filter by status, and above all a script can read the sheet and generate other artefacts from it.
That last one matters most. Keep the spec, the GTM container, and the front-end type definitions in sync by hand across three places and they will drift. Generate all three from one source and they cannot. That only works if the sheet is shaped so a machine can read it too.
Add a column when its absence makes a human answer the same question repeatedly
Columns are not free. They have to be filled, and an unfilled column is a lie. So one test keeps it simple. When this cell is blank, does a person end up answering the same thing by hand every time? If so, make it a column.
| Column | The question a human answers without it | Example values |
|---|---|---|
| Owner | This is not firing, so who fixes it? | FE / BE / GTM / GA4 automatic |
| Status | Is this already running, or do we build it? | Live / To build / To fix / Backlog |
| Priority | What goes into the next sprint? | 1 to 5 |
| GA4 scope and type | Which scope do we register the custom dimension under? | event · string · custom |
| Parameter status | If this value is empty, is that a bug or normal? | Required / Optional / Excluded / Trigger only |
| Timing and condition | At what moment is this number stamped? | Right after a successful server response |
| PII rule | Is personal data allowed on this event? | No raw values, pseudonymous join keys only |
| Dedup and quality | Could the same action be counted twice? | One per item, deduped by event_id |
Of these, Owner earns its keep most often in practice. Every defect splits four ways: regenerate the sheet and you are done, publish the GTM container, ship a front-end release, or hand it to the backend team. Those are completely different timelines. With an owner column that classification stops being a meeting and becomes a lookup.
Write example values in the real format. One identifier had its example written as DB-20260608-001 when the real value was the integer 7. Implement that literally and it is wrong. For the same reason, say whether a value is deterministic or random. Write it as evt_ab12cd34 and a developer will generate a random string, which breaks the server-side deduplication described further down.
The real decision is whether to send the value at all
This is the fork the article turns on, and the thing I spent longest on while building the sheet. You hold one value and make the same judgement over and over. Do I put this on the event as a parameter, or leave it off and stitch it back later from another table?
First, why you cannot skip the judgement entirely. GA4 has limits, and the numbers look like this. These were confirmed in Google's official documentation.
| Limit | Standard property | Analytics 360 |
|---|---|---|
| Event parameters per event | 25 | Same |
| Event-scoped custom dimensions | 50 | 125 |
| User-scoped custom dimensions | 25 | 100 |
| Item-scoped custom dimensions | 10 | 25 |
| User properties | 25 per property | Same |
| Parameter value length | 100 characters | Same |
You rarely bump into 25 per event. What actually squeezes is the custom dimension slots. Fifty event-scoped dimensions fill up faster than you expect on an account with more than forty events. And once you hit the ceiling, deleting a dimension means waiting 48 hours before you can add a new one. Two days is the cost of undoing a bad registration.
Still, the limits are not the real reason for the judgement. The real reason is this. A value you send stays attached to that event, and a value you leave off needs a guaranteed way back. That is why parameters marked as excluded stay on the sheet as rows instead of being deleted. Not sending something is itself information.
There are five reasons not to send
Fourteen parameters in the 46-event sheet were marked as not sent (12 excluded plus 2 used only for trigger branching), and once I counted, the reason was not a single one. It was five, and each reason has a different way back.
Excluded is not one thing. Why it was excluded changes entirely what remains to be done later.
Reasons 1 through 4 are effectively free. Nothing is lost, because the information already lives somewhere else or was never information to begin with. Reason 2 trips people up most. If a content-engagement event fires on "50% of the body reached and 30 seconds of active time," then the 50 and 30 ridden along on that event are always 50 and 30. You are sending an unchanging value a million times.
Reason 5 is the only problem. And it is the spot most often judged wrongly in practice.
Is the join key deterministic, or a time window?
The classic inheritance case. Say a user taps a call button on a product page. The button position and page path ride on the web event. A few seconds later the call actually connects, and that is not a web event, it is an event the server fires from the telephony side.
The server does not know the button position. That happened inside a browser. Which creates two paths. Either the front end carries the button position all the way through to the server, or you leave it off and stitch the two tables together afterwards.
Image: Crossing the same distance by bridge is not the same as crossing on a wide band. On the band, the landing point is never fixed.
When you pick the second path, one thing decides the outcome: what you join on. And join keys come in two kinds with very different properties.
Both kinds of join live in the same sheet, and on the sheet they both just read "join."
A deterministic key is something like a lead identifier, one value pointing uniquely at one thing. If the web submission event carries a lead number and the activation event carries the same lead number, the button that started the consultation comes back in a single join. Nothing is lost.
A time-window join is a different animal. You match on the phone number shown and the click timestamp, and when two people tap the same number within a minute there is no way to tell which click belongs to which call. And here is the thing: the query raises no error. It picks one, or attaches both, and the report shows perfectly reasonable numbers. Structurally identical to what makes offline conversions hard to stitch.
So the decision rule settled like this.
| Join key available | Decision | What goes on the sheet |
|---|---|---|
| A deterministic key already rides on both events | Do not send | The key name and which event it inherits from |
| A deterministic key can be created (issue a click id) | Create the key first | Who issues the key and how it travels |
| Only a time window exists | Send it | Why you send it, to block future removal proposals |
| The table to join against does not exist yet | Send it | Revisit once a join target exists |
Row three is the one that matters. Write down why you decided to send it, or in six months someone will call it redundant and propose removing it. The counterargument has to live inside the sheet.
The same rule applies to user properties. There were nine candidate user properties, and exactly one, signup method, was actually sent to GA4. Login state is already answered by a GA4 default dimension once User-ID is set. The device identifier has far too many distinct values, so it stays a BigQuery join key only. Marketing consent is not an analysis dimension, it is a control signal that switches server actions on and off. Cumulative conversion count keeps changing, so it belongs in a query at read time. Every reason different. User properties showing up as undefined usually comes from skipping this cleanup and registering properties with nothing supplying their values.
Why there are two name columns
Build the sheet long enough and one more column becomes necessary: the moment the name already baked into the code differs from the name you want in GA4.
That happened here. The site already emitted names like view_product, click_content, and view_category, while GA4's recommended events had standard names for the same meanings. Use the standard names and GA4's default reports and product funnel light up on their own. Skip them and everything has to be built as custom.
Changing a name means editing the front end and shipping. But with GTM in the middle, no release is needed. Leave the name the code emits alone, and rename only on the way out to GA4.
Original event name GA4 output event name
-------------------- --------------------
view_product -> view_item
view_category -> view_item_list
click_content -> select_content
click_event_banner -> select_promotion
share_referral -> share
submit_accepted -> qualify_lead
order_canceled -> close_unconvert_lead
It looks like code but it is just a mapping table. On the left, the name that actually fires inside the browser. On the right, the name that appears in GA4. Three generations of accumulated naming absorbed without a front-end release, and that is not free.
The price is that two names now exist in the world. The name a developer sees in the browser console differs from the name a marketer sees in GA4. Somebody will inevitably build a funnel on the left-hand name and fail to find the event in the report. So both columns sit side by side, with a line at the top of the sheet saying which is which.
There is one more trap in the same spot. While renaming, GTM can also split one event into two. If a single submission covers both a standard consultation and a self-service signup, the front end fires once and GTM reads the type value and fires exactly one of two tags, mutually exclusive.
The front end must not fire twice here. A developer who hears "we decided to split it" may helpfully push a second event, and since GTM is already splitting, conversions double. So the front-end task cell says explicitly: "no separate push, keep the single original event." Leave it out and someone will be helpful.
Put the reversal next to the advance
Almost everyone designs only the advance when building conversion events. Lead submitted, qualified, activated. Stop there and the ad platforms learn something strange.
Measure only advances and never reversals, and the ads learn how to produce leads that convert and then cancel.
In commerce terms it is sending purchases without refunds. In commerce that is obvious, yet in lead-based businesses I regularly see accounts with the reversal events missing entirely, because the cancellation happens inside the CRM.
A reversal event is not an appendix added later. It is the pair you define in the same sitting as the forward event.
Nail down the conversion unit first
The most important thing in that diagram is that the last cell is a different colour. One submission can carry several products, and each product is activated and cancelled separately. Which means the conversion unit shifts partway through, from lead to item.
Leave that undecided and the count you send to ad platforms will never match the backend count, permanently. One side counts customers and the other counts contracts. The answer is not which one is correct, it is writing down which one you chose.
Once chosen, the dedup key follows. It is the key that stops the same conversion arriving twice when you send conversions from the server.
# The dedup key is deterministic, never random.
# Resending the same event must produce the same key.
Lead-level conversion
event_id = evt_{lead_no}
Item activated
event_id = activation_{lead_no}_{item_no}
Item activation cancelled
event_id = activation_cancel_{lead_no}_{item_no}
Lead disqualified
event_id = disqualify_{lead_no}_{reason_code}
Connected call
event_id = call_{call_record_no}
One rule to remember. Resending the same event must produce the same key. Use a random string and every retry creates a brand new conversion. Which is why example values in the spec must never look like evt_ab12cd34, and must be written evt_7, in the real format.
There may be more than one status system. I have seen the web application status, the CRM consultation status, and the CRM activation status all running separately with value sets that do not overlap at all. A design that tried to express the whole funnel through a single status field broke on exactly that. Before designing conversion events, first establish who records each status. And do not trust the status values listed in a document. In one case the seven example statuses existed in none of the three real systems, and even the spellings differed.
Bring the layer outside the site into the taxonomy
Everything so far has been about the site itself. But the piece on tracking links beyond your site produced two events: checkpoints planted where the platform belongs to somebody else but the links in and out are yours to place, like a blog or video channel you operate.
You cannot just bolt these on. Left outside the taxonomy they inherit none of the column rules, and in six months nobody knows why they exist. Give them a proper place on the sheet.
Putting them on the sheet is not just giving them a slot. It means they inherit the same column rules as everything else.
Adding them forced a new value into one column: Owner. Not the front end, not the backend, not GTM, the redirect server is what records these. Across 46 events four owner values had been enough, and admitting one layer outside the site produced a fifth.
| Event | Parameter | Status | Meaning |
|---|---|---|---|
owned_media_enterArriving at owned media via an external link | media_type | Required | Which kind of owned media |
content_id | Required | Which post within it | |
entry_source | Required | Where this link was placed | |
link_id | Excluded | An entrance has no outbound link | |
owned_media_exitLeaving owned media for our own site | media_type | Required | Which kind of owned media |
content_id | Required | Which post it was left from | |
link_id | Required | Which link inside it was clicked | |
entry_source | Excluded | Inherited from the entry event of the same visitor id |
Building the table, I applied the previous section's rule to these two rows directly. The exit event's entry source is excluded. The same visitor id is already stamped on the entry event, and a visitor id is a deterministic key. That is reason 5 of the five, but on the safe side of it, because the join key is deterministic rather than a time window.
The owned media type, on the other hand, gets sent. It looks derivable from the link identifier, except the table mapping link identifiers to media types is a link registry a human maintains by hand. That relationship is not machine-guaranteed, so it will drift within six months. Do not defer to a join whose other side is maintained by a person.
The dedup key follows the same format as everything else. Use the number the redirect server issues on each click.
owned_media_enter event_id = owned_in_{click_no}
owned_media_exit event_id = owned_out_{click_no}
# What ties the two events to one person is the visitor id.
# A click number is issued per click; the visitor id persists.
These two rows are not implemented yet. They are a design proposal that has been given a slot and had its columns filled, not something fired with data behind it. The status cell reads backlog. What connection rate they would actually produce depends, as the tracking links piece says, on browser policy, so the only way to know is to measure it.
If the link naming convention itself collapses, this table collapses with it. Link issuance conventions are covered separately in the UTM convention piece.
Where it falls apart after the sheet is done
A finished sheet feels like the end, but the drift starts right after. Four spots came up repeatedly in audits.
One, the name matches but the variable behind it does not. An exhaustive name-by-name comparison between spec and code once returned zero mismatches, and that result was overturned. Every name matched, and one parameter was reading the wrong variable. An inventory comparison only checks names. The comparison is not finished until you check which variable each parameter reads.
Two, omit a key and the previous value is read. The GTM data layer merges and persists values between pushes. So if an event omits a key, the previous event's value simply stays and gets read. No error, and a plausible value arrives. The spec has to state "always send the key, even when the value is empty."
Three, the collection moment differs from reality. A spec said "when the video starts playing" while the code fired on entering the view, because the player was an embed and playback start could not be detected. There are two honest answers. Change the code to match the spec, or change the spec to match reality and record that you did. Leave it unresolved and whoever reads that metric reads it entirely wrong.
Four, hand-editing the derived artefacts. Edit the generated GTM container or code type definitions directly in a UI and the next regeneration reverts them. The place to edit is always the sheet.
None of these four is prevented by building a good sheet, which is why the last column is last verified. If there is no record of when a row was checked against the real thing, that row is a document, not a fact.
Open one sheet today
Whether you are defining events fresh or auditing existing ones, the order is the same. Work through these five.
- Write the questions first: list five questions you currently cannot answer. Does each answer come out as a rate or a number? If not, rewrite it.
- One sentence per event: if you have an existing event list, write the question each one answers beside it, in one sentence. How many cannot be written? Those are your retirement candidates.
- Check against the screens: for each event, point at "this element, on this screen." If you cannot, classify it as entry/impression, server-side, or design running ahead of the screens.
- Invert the row structure: if parameters are listed inside a cell, expand to one row per event-parameter pair. Then mark each parameter required, optional, or excluded.
- A reason and a join key per exclusion: beside every excluded parameter, write why it is not sent and how it comes back. If the join key is a time window, cancel the exclusion and just send it.
If you only pick one of the five, pick the second. When half the list has no one-sentence question, that taxonomy has not started yet.
The length of an event list is not maturity. The ability to retire events is.
This is an advanced instalment in the digital marketing analytics series. The overall map lives in the five-layer measurement audit, and if the event as a unit is unfamiliar, start with cookies, sessions, and events. The story beyond the site continues in tracking links and MMPs and measuring off-page impressions, and link naming rules are in the UTM convention piece.
Sources
- 25 parameters per event, 25 user properties, 100-character parameter values, 40-character event names: Google Analytics Help, Event collection limits
- Custom dimension limits (50 event, 25 user, 10 item; 125, 100, 25 on 360) and the 48-hour wait after deletion: Google Analytics Help, About custom dimensions and metrics
- Starting from questions, narrowing to a handful of core events, standard tracking plan columns: Amplitude, How to Create a Tracking Plan
- The Object-Action framework for event naming and the consistency principle: Segment (Twilio), Naming conventions for clean data
- GA4 recommended event names and the events wired into the product funnel: Google Analytics Help, Recommended events
The sheet structure, column design, and exclusion judgements in this article come from a 46-event sheet I actually ran and audited. The services, products, identifiers, and amounts in the examples have all been generalised, and what transfers is the column structure and the decision rules rather than the values. The GA4 limits are numbers confirmed in Google's official documentation rather than ceilings I hit myself, and since policies change it is worth rechecking the source at the time you implement. The two owned-media events in the final section are a design proposal that has been given a slot on the sheet and not yet implemented. Rates such as join loss and connection rate were never measured, so no figures are given for them.
Found this useful? Share it
Latest posts
Kakao Share Thumbnail Not Showing? Here's the Fix (JavaScript SDK 2025)
Learn why Kakao Share thumbnails don't appear even with a valid imageUrl, and fix it using scrapImage to convert your URL into a KAGE CDN URL. Step-by-step guide.
How to Generate CRM User Manuals in 20 Minutes with Claude Code Sub-Agents
Learn how Claude Code sub-agents can analyze your CRM codebase and auto-generate user manuals and visual guidebooks in just 20 minutes. See the real-world results.
GEO Generative Engine Optimization: The Complete 2025 AI Marketing Strategy Guide
Learn what GEO (Generative Engine Optimization) is, how it differs from traditional SEO, and discover 5 proven AI marketing strategies to get cited by AI search engines in 2025.
Related projects
Get new posts by email
Insights on marketing, analytics, and dev, delivered to your inbox.

