RESOURCES

Free tools I wish existed when I started.

From real enterprise engagements. No email gate. Bookmark and use.

CHECKLIST

GTM audit checklist

The audit I run in week one of every engagement. Order matters — inventory before judgment.

1. Inventory (find everything that fires)

2. Data layer

3. Triggers & tags

4. Consent & privacy

5. QA & governance

The audit always finds tags nobody remembers adding. Inventory before architecture.
GUIDE

GA4 event naming guide

Conventions that keep a property queryable two years and three teams later.

Rules

Why it matters downstream

Every event name becomes a value in BigQuery's event_name column. Inconsistent naming means every SQL query starts with a CASE WHEN cleanup block — forever. Naming is a data engineering decision disguised as a marketing one.

Budget one custom dimension registry per property. GA4's limits (50 custom event-scoped dimensions) run out faster than teams expect.
SQL LIBRARY

BigQuery SQL library for GA4

Copy-paste starting points for the GA4 export. Test on bigquery-public-data.ga4_obfuscated_sample_ecommerce.

Event deduplication

-- GA4 export can contain duplicate events; dedup on a composite key
SELECT * EXCEPT(rn) FROM (
  SELECT *, ROW_NUMBER() OVER (
    PARTITION BY user_pseudo_id, event_name, event_timestamp,
      (SELECT value.int_value FROM UNNEST(event_params)
       WHERE key = 'ga_session_id')
    ORDER BY event_timestamp
  ) AS rn
  FROM `project.dataset.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260131'
) WHERE rn = 1;

Session table (the export doesn't have one)

SELECT
  user_pseudo_id,
  (SELECT value.int_value FROM UNNEST(event_params)
   WHERE key = 'ga_session_id') AS session_id,
  MIN(event_timestamp) AS session_start,
  MAX(event_timestamp) AS session_end,
  COUNTIF(event_name = 'page_view') AS pageviews,
  COUNTIF(event_name = 'purchase') AS purchases
FROM `project.dataset.events_*`
GROUP BY 1, 2;

Reconciliation vs the GA4 UI

-- When stakeholders say "BigQuery doesn't match GA4":
-- 1. Same date range in the property timezone, not UTC
-- 2. UI counts sessions via approximation (HLL); export is exact
-- 3. Consent Mode modeled data exists ONLY in the UI, never in export
SELECT COUNT(DISTINCT CONCAT(user_pseudo_id,
  (SELECT value.int_value FROM UNNEST(event_params)
   WHERE key = 'ga_session_id'))) AS sessions_exact
FROM `project.dataset.events_*`
WHERE _TABLE_SUFFIX = '20260115';
Full versions with tests live in my GitHub pipeline repo — including funnel and attribution models.
GUIDE

Power BI performance guide

Why your dashboard is slow — in the order you should check.

1. Model (80% of problems)

2. DAX

3. Visuals

4. Refresh & storage

Rule from the SAP BEx migration: transform in the warehouse, model in the semantic layer, decorate in the report. Slow dashboards almost always violate the first step.