본문으로 건너뛰기
Back to blog
Digital Marketing Analytics

46 Events, 14 Parameters We Chose Not to Send: How to Design 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.

29 min readDigital Marketing Analytics
event taxonomyGA4 event designtracking planevent specGTM eventscustom dimensionsBigQuery joinconversion deduplicationdata governance
46 Events, 14 Parameters We Chose Not to Send: How to Design an Event Taxonomy

For: marketers and PMs who use GA4 and GTM but have never built an event spec  ·  Format: explainer plus an audit case  ·  2026-08-06

TL;DR

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_..._modal was 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_category was reading a variable called category, 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.

Humans decide these two. Source of truth. 1. Business questions In answerable form 2. Journey mapping Screen by screen 3. Event taxonomy This article 4. Tracking plan The doc devs receive 5. Build and audit FE · BE · GTM 6. Governance Who fixes it on change Step 3 onward is derived from 1 and 2. When spec, container, and code disagree, the fix belongs upstream.

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.

Common shape: 1 event = 1 row Event Parameters view_item item_id, item_name, item_brand, item_category, entry_point ... generate_lead lead_id, event_id, value ... Every parameter has its own status, type and source, with nowhere to record them. What we use: event × parameter = 1 row Event Parameter Status view_item item_id Required item_name Optional entry_point Excluded generate_lead lead_id Required Status, type, source, PII rule all attach to the row. What you do not send stays on the sheet too. More rows is not the cost. Hiding information inside a cell is the cost. 46 events become 179 rows, and that is what makes the sheet machine-readable.

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.

ColumnThe question a human answers without itExample values
OwnerThis is not firing, so who fixes it?FE / BE / GTM / GA4 automatic
StatusIs this already running, or do we build it?Live / To build / To fix / Backlog
PriorityWhat goes into the next sprint?1 to 5
GA4 scope and typeWhich scope do we register the custom dimension under?event · string · custom
Parameter statusIf this value is empty, is that a bug or normal?Required / Optional / Excluded / Trigger only
Timing and conditionAt what moment is this number stamped?Right after a successful server response
PII ruleIs personal data allowed on this event?No raw values, pseudonymous join keys only
Dedup and qualityCould 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.

LimitStandard propertyAnalytics 360
Event parameters per event25Same
Event-scoped custom dimensions50125
User-scoped custom dimensions25100
Item-scoped custom dimensions1025
User properties25 per propertySame
Parameter value length100 charactersSame

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.

Why it is not sent How you get it back 1. GA4 already collects it page_path Nothing to do at all. It is already a default dimension. 2. It is the firing condition max_scroll_percent · active_time_seconds Nothing to recover. A constant carries no information. Change it, change the event. 3. The event name already splits it cta_selected Split by event name and you are done. No reason to carry it twice. 4. Used only for trigger branching lead_type GTM reads it, splits into two events, drops it. The split result is the value. 5. Inherited from another event entry_point · entry_page_path · button_location Only this one is risky Only a BigQuery join brings it back. Everything rides on the join key. A broken join loses the value silently Reasons 1 to 4 cost you nothing. Only reason 5 leaves work behind.

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.

Two pairs of navy plateaus separated by a gap. On the left pair a single orange bridge connects one point to exactly one point, while on the right pair a wide translucent orange band spans the gap with many threads tangled inside so it is impossible to tell which thread reaches which point, drawn as a flat vector illustration

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.

Deterministic key, safe to leave off Web lead submitted entry_point present lead_id Activation (server) no entry_point Exactly one row binds to one row Time-window join, drifts silently User A taps call 14:02:10 · bottom nav User B taps call 14:02:38 · sticky bar number + time 1 connected call same number, 14:02 to 14:03 Which of the two is never determined and no error is raised With a deterministic key you can leave it off. With only a time window, lean toward sending it. Splitting across more numbers lowers the collision rate, but never to zero.

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 availableDecisionWhat goes on the sheet
A deterministic key already rides on both eventsDo not sendThe key name and which event it inherits from
A deterministic key can be created (issue a click id)Create the key firstWho issues the key and how it travels
Only a time window existsSend itWhy you send it, to block future removal proposals
The table to join against does not exist yetSend itRevisit 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.

Advance Lead submitted Lead level · 1 event Lead qualified Lead level · 1 event Activation confirmed Item level · N events Reversal Lead disqualified Absent · wrong number · declined Junk-lead rate by channel Cancelled pre-activation Item level Drop-off after signing Cancelled post-activation Item level · negative value Subject to clawback Do not leave the cell under a forward event empty. Empty cells stay empty forever. Where the conversion unit shifts from lead to item is where the dedup key design branches.

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.

One taxonomy sheet Off site (L1) Recorded by: redirect server owned_media_enter media_type · content_id · entry_source owned_media_exit media_type · content_id · link_id On site (L2 to L4) Recorded by: GTM · server page_view · view_item The existing 46 events generate_lead · activation Conversions · reversals Only one thing runs through both areas: the visitor id. So the join key column matters most on these two rows. The owner column gains a new value. Not FE, not BE, not GTM, but the redirect server.

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.

EventParameterStatusMeaning
owned_media_enter
Arriving at owned media via an external link
media_typeRequiredWhich kind of owned media
content_idRequiredWhich post within it
entry_sourceRequiredWhere this link was placed
link_idExcludedAn entrance has no outbound link
owned_media_exit
Leaving owned media for our own site
media_typeRequiredWhich kind of owned media
content_idRequiredWhich post it was left from
link_idRequiredWhich link inside it was clicked
entry_sourceExcludedInherited 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.

Event taxonomy, a five-step checklist
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

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

Related projects

Get new posts by email

Insights on marketing, analytics, and dev, delivered to your inbox.