Hertz Maroc — Rescuing a Botched Server-Side Migration with dbt
A previous agency migrated Hertz Maroc to server-side tracking and Consent Mode V2 — and broke two years of Power BI reporting in the process. The migration renamed every funnel event to Google's recommended schema without accounting for the GA4 → BigQuery → Power BI pipeline, and shipped a consent banner that left dismissed sessions in an undefined state. We rebuilt the foundation with a dbt reconciliation layer and a properly configured consent flow.
Outcomes
The Challenge
An agency hired before us had migrated Hertz Maroc to server-side tracking and rolled out Consent Mode V2 in the same sprint. Both pieces of work were technically delivered — but neither accounted for the fact that Hertz Maroc had been exporting GA4 to BigQuery for two years, with all of their executive Power BI reports built on top of that historical event stream.
The agency renamed every funnel event to Google's recommended schema (view_item_list, select_item, begin_checkout, purchase, etc.) without realising the legacy Power BI queries depended on the old event names. Every dashboard broke overnight. On top of that, the consent banner left dismissed sessions in an undefined state — neither granted nor denied — so a meaningful share of transactions landed in GA4 with no transaction ID, no amount, no booking metadata at all. The reports that hadn't broken on the rename broke on the data holes.
Our Approach
We didn't rebuild the tracking plan from scratch — that would have created a third version of the schema and made the problem worse. Instead, we treated the v1 (legacy) and v2 (post-migration) event streams as two sources to reconcile in the warehouse, and built a dbt staging layer that maps every v1 event name to its v2 equivalent and vice versa. From dbt downstream — Power BI included — there is one canonical schema, and the historical two years of v1 events flow through it as cleanly as the new v2 events.
In parallel, we fixed the Consent Mode V2 misconfiguration: the dismissal action was wired to an explicit denied state with the proper signal flags, the banner copy was adjusted so users have to make a choice, and the server-side container was updated to respect the consent state per-destination. Within two weeks the data holes closed, the Power BI reports came back to green on the refreshed dbt models, and Hertz Maroc had its decision-making layer back.
Inside the engagement
This is a rescue story. Hertz Maroc, the Moroccan franchise of the global car rental brand, had hired an agency to handle two pieces of work in the same sprint: migrate their analytics to server-side tracking, and roll out Consent Mode V2 to meet new EU consent requirements. Both pieces were delivered on time. Both pieces broke things in ways the agency had not anticipated — and that nobody in the business noticed until the monthly executive review, when the Power BI dashboards refused to load the previous month's data.
What the agency changed
The migration was structurally sound on paper. The agency stood up a server-side GTM endpoint, moved the major paid platforms (Google Ads, Meta) to server-side conversion APIs, and updated the on-site GTM container to fire events through the server. All of that was correct.
The problem was that, as part of the migration, every funnel event was renamed to follow Google's recommended naming convention. The legacy schema — which had been in place for more than two years and had names like vehicle_search, vehicle_view, booking_start, booking_complete — was replaced overnight with the Google-aligned schema: view_item_list, select_item, begin_checkout, purchase. Cleaner from a Google Ads UI perspective, but a complete break with everything downstream.
The Consent Mode V2 rollout was the second piece. Google's CMP signal grid was wired up in GTM. The banner appeared on the site. The flags were transmitted to GA4 and Google Ads. On the surface, it worked. Underneath, the dismissal behaviour had not been pinned down: users who closed the banner with the X button, or clicked elsewhere on the page without making a choice, were left in an undefined consent state. Tags fired anyway, but with a hash of missing parameters, because the consent gating logic wasn't sure what to do with them.
What broke for the business
Hertz Maroc had been exporting GA4 to BigQuery for two years. The export was the source for eight Power BI reports — fleet utilization by station, booking conversion by acquisition channel, average rental duration by customer segment, lead-time to pickup, and the executive monthly P&L reconciliation dashboard that the CEO reviewed every first Monday.
The Power BI queries were written against the legacy event names. When the v2 schema landed in BigQuery, every query that filtered on event_name = 'booking_complete' started returning zero rows. The queries that joined on event_params.vehicle_category returned NULL because that custom dimension had been renamed too. Half the reports broke entirely. The other half kept running but on bad data — they pulled in the few legacy events that still occasionally fired (because some pages on the site hadn't been redeployed) and ignored the v2 events that the actual users were generating.
And then there were the holes. Roughly a quarter of the transactions arriving in GA4 had no transaction ID, no booking amount, no vehicle category, no station of pickup — nothing. The data was syntactically valid (the events were firing), but semantically empty (the consent state had stripped everything sensitive). The finance team flagged it first: the GA4 monthly revenue total no longer matched the booking platform total, and the gap was growing.
By the time we were brought in, the team had been three weeks without trustworthy reporting. The agency had been asked to fix it and had proposed rebuilding the v2 schema again — a third version — which would have compounded the problem. Hertz Maroc reached out for a second opinion.
Our diagnostic
We spent five days on diagnostic before touching anything. The deliverables of that week were three documents that everyone could see:
- A side-by-side mapping of every v1 event and every v2 event, with the corresponding event parameters and custom dimensions in each. Sixty-three events in total across the funnel. Most had a clean 1:1 mapping. A handful of v1 events had been split into two v2 events. Three v2 events were genuinely new (not present in v1).
- An audit of the consent mode V2 configuration — which signals were being sent, in which state, by which path of user interaction. The dismissal-without-choice path was the only one in an undefined state. Everything else was either explicit-grant or explicit-deny and was wired correctly.
- A list of every Power BI query, the v1 events it depended on, and the v2 mapping that would be needed for it to keep working. Eight reports, 41 queries, all mappable.
The conclusion was straightforward: we did not need a new tracking plan, we needed a reconciliation layer in the warehouse and a fix to the consent gating. Two work streams, two weeks of execution.
Fix 1 — A dbt reconciliation layer between v1 and v2
We built a dbt project on top of the BigQuery GA4 export with three layers, matching the standard dbt convention:
- Staging — one model per source:
stg_ga4_events_v1(legacy, frozen at the cutover date) andstg_ga4_events_v2(post-migration, ongoing). Each staging model normalises column types, flattens the GA4 event_params struct, and tags every row with the schema version. - Intermediate —
int_events_canonicalapplies the v1 ↔ v2 mapping. Every event name and every parameter is normalised to a single canonical schema. The mapping is a YAML file (event_mapping.yml) that lists the 60+ events explicitly: v1 name, v2 name, canonical name, parameter mappings. Adding a new event later is a one-line change. - Marts — the consumable layer:
fct_bookings,fct_funnel_steps,dim_vehicles,dim_stations,mart_executive_kpis. These are what Power BI reads from. The marts know nothing about v1 or v2 — they only see the canonical schema.
The result was a single unified event timeline that runs from the start of the GA4 export through the migration and into the present, with no break. Power BI was re-pointed from the raw GA4 export to the dbt marts. All 41 queries were rewritten against the canonical schema (a one-day job once the marts were in place). All 8 reports came back online.
Fix 2 — Consent Mode V2 done properly
The consent fix was less code and more configuration. Three changes:
- The dismissal action (X button, click outside the banner, navigate away) was explicitly wired to denied state — not undefined. The CMP we deployed defaults to denied if no explicit choice is made within a session, which is also the safer EU-aligned default.
- The banner copy was rewritten to make the choice unavoidable. The X button was removed in favour of explicit Accept and Decline CTAs of equal visual weight. Compliance reviewed and signed off.
- The server-side container was updated to respect consent state per destination: granted users flow through to Google Ads, GA4, Meta CAPI, and the warehouse; denied users still hit the warehouse for first-party logging (so the team retains aggregate counts and operational signal) but stop short of any third-party destination. PII is hashed before it ever enters the warehouse.
The data holes closed within the first week. Transactions arriving in GA4 now consistently carry their full payload — transaction ID, amount, vehicle category, station, customer segment — because either the user has granted consent (in which case the payload flows) or they have denied (in which case the transaction does not fire in GA4 at all, but is logged aggregate-only in the warehouse). The hybrid undefined state, which is the dangerous one, is gone.
Backfill and reporting restoration
The dbt marts were backfilled across the full two-year window of historical data. Cohort retention, lead-time analysis, fleet utilization by station, and the executive monthly P&L reconciliation were all re-run against the canonical schema. Where the historical data had legitimate gaps — pre-migration months with no v2 events, for example — the marts represent those as nulls or zero-counts in the appropriate columns rather than masking them.
The CEO's first Monday review went back to its normal cadence. The finance team's GA4-to-booking-platform reconciliation came within 1% of parity — the residual gap is the legitimate denied-consent fraction, which is now visible, labelled, and accepted as part of the compliance reality.
What we would do differently next time
The single best decision in this engagement was the five-day diagnostic before touching anything. Without the explicit v1 ↔ v2 mapping document in hand, we would have been tempted to add band-aids on top of the broken setup, and we would have made it worse.
What we would change: we would push, when scoping similar migration rescues, for a brief data freeze window on the production tracking during the dbt build. Hertz Maroc's data kept flowing during the rescue (correctly — the business was still running), and we had to keep updating the staging models to catch the most recent events. A two-day freeze would have shortened the build by a few days. Not always feasible for a business that lives off online bookings, but worth asking for.
The bigger structural lesson is the one this case illustrates for anyone considering a tracking plan migration: the names are the schema, and the schema is a contract with every consumer downstream. Power BI is a consumer. Looker is a consumer. The CRM is a consumer. Every paid platform is a consumer. Renaming events is never a purely cosmetic change. Before any rename ships, the answer to "what breaks downstream?" needs to exist on a piece of paper that the business has signed off.
More from the field
PDS Shop — Closing the 30% Gap Between Shopify and Google Ads with Stape.io
A French online perfumery on Shopify was looking at a 30% gap between the transactions logged in their Shopify admin and the conversions reported by GA4 and Google Ads — every day, growing wider every quarter. We deployed server-side tracking via Stape.io, layered CAPI on top of the existing pixels, and closed the gap to under 5% within three weeks.
Prestigia.com — Rebuilding the Data & Activation Stack of a Premium OTA
A premium online travel agency selling primarily into the US market had a fundamentally broken tracking stack — Firebase reporting wrong conversion values, a 140-tag GTM container firing false data, and no real funnel visibility. We rebuilt the foundation: business-grade datalayer, server-side tracking, BigQuery warehouse, and a Klaviyo-driven retention engine.
