Before you use an example: Field names depend on your connector and setup. Calculated expressions, SQL queries and configuration steps are labelled separately; copy one example at a time. Examples using raw counts or session-level fields require a modelled source rather than the native GA4 connector. Date ranges, identity, attribution, missing data and aggregation must match the question you want to answer. SQL examples use completed GA4 daily exports and exclude missing session identifiers where required.
Looker Studio

Looker Studio Calculated Fields

Looker Studio REGEXP_MATCH · Grouping

EU Countries - Combined Group

Returns a single label for all 27 EU member states, so they aggregate as one row alongside individually-listed non-EU countries. Case-insensitive via (?i).

Calculated field: check input names and types
CASE
  WHEN REGEXP_MATCH(Country, "(?i)^(Austria|Belgium|Bulgaria|Croatia|Cyprus|Czech Republic|Czechia|Denmark|Estonia|Finland|France|Germany|Greece|Hungary|Ireland|Italy|Latvia|Lithuania|Luxembourg|Malta|Netherlands|Poland|Portugal|Romania|Slovakia|Slovenia|Spain|Sweden)$")
    THEN "European Union"
  ELSE Country
END
Usage: Use this dimension with additive metrics such as event counts. Grouping labels does not guarantee that distinct-user totals can be summed safely across countries. Check the country names returned by your connector.
regexgeographygroupingCASE
Looker Studio REGEXP_MATCH · Boolean

Is EU Country (Boolean)

Returns TRUE/FALSE - useful as a chart filter or segment condition without changing the Country dimension itself.

Calculated field: check input names and types
REGEXP_MATCH(
  Country,
  "(?i)^(Austria|Belgium|Bulgaria|Croatia|Cyprus|Czech Republic|Czechia|Denmark|Estonia|Finland|France|Germany|Greece|Hungary|Ireland|Italy|Latvia|Lithuania|Luxembourg|Malta|Netherlands|Poland|Portugal|Romania|Slovakia|Slovenia|Spain|Sweden)$"
)
Usage: Filter this field to TRUE or FALSE. This example matches country names, not ISO codes. If your source uses codes, supply the complete code list separately.
regexgeographybooleanfilter
Looker Studio REGEXP_MATCH · URL

Commodity Detail Pages (10-digit ID)

Matches pages at /commodities/ followed by exactly 10 numeric digits - commodity product detail pages. Anchored to prevent false matches on longer IDs.

Calculated field: check input names and types
REGEXP_MATCH(
  Page path + query string,
  "^/commodities/[0-9]{10}([/?].*)?$"
)
Matching: Accepts exactly 10 digits followed by the end of the value, a slash or a query string. The trailing wildcard accommodates full-value matching in REGEXP_MATCH; an 11-digit ID does not match.
regexpage-pathurlcommodity
Looker Studio REGEXP_MATCH · URL

Form Submit Pages - Multi-Section Match

Matches relevant URL paths. The formula does not identify a form submission by itself; combine it with an event-name filter.

Calculated field: check input names and types
REGEXP_MATCH(
  Page path + query string,
  ".*(find_commodity|/commodities/|/headings/).*"
)
Usage: Filter Event name to form_submit AND this field to TRUE. Confirm that form_submit represents a successful submission in your implementation. If query-string values must not influence matching, use Page path instead.
regexformpage-pathcontains
Looker Studio CASE · URL Bucketing

Page Section Bucketing

Groups page paths into named sections for high-level traffic analysis. Ordered from most to least specific - regex evaluation is top-down.

Calculated field: check input names and types
CASE
  WHEN REGEXP_MATCH(Page path, "^/commodities/[0-9]{10}(/.*)?$") THEN "Commodity Detail"
  WHEN REGEXP_MATCH(Page path, "^/commodities(/.*)?$") THEN "Commodities"
  WHEN REGEXP_MATCH(Page path, "^/headings(/.*)?$") THEN "Headings"
  WHEN REGEXP_MATCH(Page path, ".*find_commodity.*") THEN "Find Commodity"
  WHEN Page path = "/" THEN "Homepage"
  ELSE "Other"
END
Order: More specific paths come first. Use a path-only input. The commodity detail rule requires a 10-digit segment and does not classify an 11-digit ID as a detail page.
regexbucketingCASEpage-path
Looker Studio CASE · Event Filtering

Click Text - Internal Links Only

Shows click_text values only for the internal_link_click event, returning NULL for all other events so your dimension stays clean.

Calculated field: check input names and types
CASE
  WHEN Event name = "internal_link_click"
    THEN click_text
  ELSE NULL
END
Prerequisite: Send click_text on your internal_link_click event, register click_text as an event-scoped custom dimension and refresh your data-source fields. Select the field by its displayed name in the connector; it may use your custom dimension display name. NULL values can still appear as an empty or (not set) row, so filter Event name as well.
click-textcustom-dimensionCASEevent
Looker Studio CASE · Event Filtering

Closest Parent Class - Internal Links

Surfaces the Value of closest parent class custom dimension, scoped to internal link click events only.

Calculated field: check input names and types
CASE
  WHEN Event name = "internal_link_click"
    THEN value_of_closest_parent_class
  ELSE NULL
END
Prerequisite: Collect value_of_closest_parent_class on internal_link_click and register it as an event-scoped custom dimension. Select the actual displayed field name in your connector. The formula does not collect a parent class or remove empty rows by itself.
click-classesparent-classcustom-dimensionGTM
Looker Studio DATETIME_DIFF · Duration

Time Between Two Events (DATETIME_DIFF)

Calculates elapsed seconds between a start event and an end event. Requires both timestamps to exist on the same row - pre-join in BigQuery if they're on separate rows.

Calculated field: check input names and types
DATETIME_DIFF(event_end_timestamp, event_start_timestamp, SECOND)
Prerequisite: Both fields must be compatible Date & Time values on the same row. BigQuery TIMESTAMP values may need conversion to DATETIME in a consistent timezone. Alternatively, use the seconds_to_commodity numeric output from the BigQuery example directly and format seconds as a duration.
DATETIME_DIFFdurationtime-betweenjourney
Looker Studio CASE · Session Segmentation

Session Count per User: Pre-aggregated Buckets

Buckets a numeric sessions_per_user field from a source containing one row per user for a defined date range. The native GA4 Sessions metric is aggregated at chart grain and cannot identify individual users or loyalty tiers.

Calculated field: modelled source
CASE
  WHEN sessions_per_user IS NULL OR sessions_per_user < 1 THEN "0 - Unknown"
  WHEN sessions_per_user = 1 THEN "1 - One session"
  WHEN sessions_per_user < 4 THEN "2 - 2 to 3 sessions"
  WHEN sessions_per_user < 8 THEN "3 - 4 to 7 sessions"
  WHEN sessions_per_user < 16 THEN "4 - 8 to 15 sessions"
  ELSE "5 - 16 or more sessions"
END
Source: Pre-aggregate distinct (user_pseudo_id, ga_session_id) pairs per user in BigQuery, excluding missing identifiers. One session in the selected period does not mean a new user. Use user counts per bucket; do not substitute the native Sessions metric.
session-bucketingreturning-usersCASEsegmentation
Looker Studio Ratio Metric · Funnel

Search-to-Commodity Session Rate

Calculates the share of search sessions followed by a commodity-detail page view. Requires a precomputed session table with one row per user/session; separate counts of search and page-view events cannot establish this rate.

Calculated field: modelled source
CASE
  WHEN SUM(has_search) > 0
    THEN SUM(has_commodity_after_search) / SUM(has_search)
  ELSE NULL
END
Source: Use has_search and has_commodity_after_search from the BigQuery same-session query below. Format as Percent. This is a session journey rate, not a click event rate. Sessions with multiple searches contribute once. Use SUM on these raw numeric flags; do not re-aggregate native GA4 ratio metrics.
conversion-ratefunneljourneyview_search_results
Looker Studio REGEXP_EXTRACT · Dimension

Extract Commodity ID from URL

Pulls the 10-digit product ID out of a URL like /commodities/1234567890 as a standalone dimension for joining or grouping.

Calculated field: check input names and types
REGEXP_EXTRACT(
  Page path + query string,
  "^/commodities/([0-9]{10})(?:[/?]|$)"
)
Boundary: The non-capturing boundary rejects an 11-digit identifier. The only capture group returns the ID. Adapt field names and check your connector's unmatched-value behaviour.
REGEXP_EXTRACTurliddimension
Looker Studio Metric · CVR

Session Key Event Rate

Measures sessions containing at least one selected key event, divided by all sessions. Key-event counts divided by sessions measure events per session and may exceed 100%; they are not a session conversion rate.

Calculated field: modelled source
CASE
  WHEN SUM(session_count) > 0
    THEN SUM(sessions_with_key_event) / SUM(session_count)
  ELSE NULL
END
Source: Prefer GA4's built-in Session key event rate when available. This custom formula requires compatible raw counts from a pre-aggregated source, with one session counted once in the numerator. Format as Percent. A key event itself makes a session engaged, so dividing by engaged sessions does not independently diagnose traffic intent or page quality.
CVRsessionsengaged-sessionsconversion
GA4

GA4 Configuration & Segments

GA4 Setup · Custom Dimension

Custom Dimension - Naming Rules

GA4 custom dimension parameter keys must match the GTM event parameter key exactly. Spaces, capitalisation, and underscores all matter.

Configuration / reference steps
GTM variable display name: Click Text
GA4 event name: internal_link_click
GTM event parameter name: click_text
GTM event parameter value: {{Click Text}}
GA4 custom dimension:
  Scope: Event
  Event parameter: click_text
  Display name: Click text

Parameter names are case-sensitive.
Use letters, numbers and underscores; start with a letter.
Avoid reserved names and prefixes.
Select the actual custom dimension field in Looker Studio.
Timing: Custom definitions do not backfill historical reporting data. Previously collected parameters can still be available in BigQuery exports. CamelCase is not inherently invalid; it simply must match the collected parameter name exactly. Do not send personal information in click text.
custom-dimensionnamingsetupgotchas
GA4 Segment · Event Sequence

User Segment: Search Followed by Detail View

GA4 sequence conditions belong to user segments. A session segment with two conditions does not establish event order.

Configuration / reference steps
Explore > Segments > New segment > User segment
Add sequence

Step 1:
  Event name exactly matches view_search_results

Step 2:
  Event name exactly matches page_view
  AND Page location matches regex:
  ^https?://[^/]+/commodities/[0-9]{10}([/?#].*)?$

Choose "indirectly followed by" to allow intervening events.
Choose "directly followed by" only if the next recorded
event must be the detail page view.

Set the sequence scope to within the same session
where that option is available in your property.
Interpretation: This includes users meeting the sequence; it is not a count of qualifying sessions only. For a defined session denominator, use the BigQuery query below. Keep both step-2 conditions scoped to the same event. Available fields and UI labels can vary.
segmentevent-sequenceuser-journeypage_view
GA4 Reference · Timing

Audience & Dimension Population Delays

Expected wait times for data to appear after creating audiences, custom dimensions, or custom events in GA4.

Configuration / reference steps
Custom dimension reporting: allow 24 to 48 hours after setup.
GA4 audience membership: allow 24 to 48 hours to populate.
Google Ads audience availability: check linked-account
status, consent, eligibility and destination processing.
Do not treat a single timing estimate as a guaranteed SLA.

Looker Studio: refresh data-source fields after registering
a dimension, then check data freshness and field compatibility.
Avoid reconnecting the property before investigating the issue.
History: Registering a custom definition does not backfill it in standard reporting. Collection, reporting and advertising audience availability are different processes. A delay does not automatically mean the tagging failed.
timingaudiencecustom-dimensiondelay
GA4 Debugging · Attribution

Why Source / Medium is (not set) but Campaign Exists

Investigate missing acquisition values using fields with the same scope. User acquisition, session acquisition and event attribution can legitimately show different sources.

Configuration / reference steps
1. Identify the exact fields: first-user, session or event scope.
2. Check the original tagged URL and every redirect.
   Confirm utm_source, utm_medium and utm_campaign survive.
3. Check consent and when the first analytics hit is sent.
4. Use DebugView to confirm received page_location and events.
5. For cross-domain journeys, configure cross-domain measurement
   and verify that the linker parameter survives navigation.
6. Use unwanted-referral settings only for appropriate referrers;
   they do not replace cross-domain measurement.
7. For Measurement Protocol, validate client_id and session_id
   association and the requirements of the intended report.
8. If exported, inspect collected_traffic_source and
   session_traffic_source_last_click in BigQuery.

Do not assume a later referral rewrites the original session source.
Do not add source/medium blindly to every Measurement Protocol hit.
UTMattributiondebuggingsource-medium
GTM

Google Tag Manager Variables

GTM Custom JavaScript Variable

Capture data-* Attribute on Click

Returns the value of any data-* attribute from the clicked element - works even when the DOM has multiple matching elements, because it uses the runtime click target.

JavaScript: GTM variable
function() {
  var el = {{Click Element}};
  if (!el || el.nodeType !== 1) return undefined;
  var target = el.closest('[data-tree-target]');
  return target ? target.getAttribute('data-tree-target') : undefined;
}
Setup: Enable Click Element. This handles clicks on a nested span or icon as well as the attributed element. Use an All Elements click trigger with Click Element matching CSS selector [data-tree-target], [data-tree-target] *, or an appropriate Just Links trigger. Verify the returned value in Preview mode.
data-attributecustom-jsclickvariable
GTM Variable → GA4 Parameter

Click Text → GA4 Event Parameter

Correct way to map GTM's built-in Click Text variable to a GA4 event parameter - the key distinction between variable display name vs. parameter key.

Configuration / reference steps
GA4 Event tag:
  Event name: internal_link_click
  Event parameter name: click_text
  Event parameter value: {{Click Text}}

Use the variable picker, or type the exact {{Click Text}} token.
Do not enter the plain text Click Text as the parameter value.

In GA4, create an event-scoped custom dimension:
  Event parameter: click_text
  Display name: Click text

Verify the collected value in Preview mode and DebugView.
Distinction: The plain text Click Text sends that literal value; it does not by itself cause (not set). Missing values or a custom-definition mismatch can cause (not set). Send only text that is safe for analytics collection.
click-textGTM-variableGA4-parametersetup
GTM DOM Element Variable

DOM Element Variable - CSS Selector + Attribute

Simpler alternative to Custom JavaScript when you only need to read an attribute from a single, unique element on the page (not the clicked element).

Configuration / reference steps
Variable Type     : DOM Element
Selection Method  : CSS Selector
Element Selector  : a[data-tree-target]
Attribute Name    : data-tree-target

-- Returns the value of data-tree-target from the FIRST
-- matching element on the page.
-- Use Custom JavaScript (above) if multiple elements exist
-- and you need to target the one that was clicked.
Scope: A DOM Element variable reads from the selected page element, not necessarily the click target. Use a unique selector and verify the value. For click-specific attributes, use the Click Element pattern above.
DOM-elementCSS-selectorattributevariable
BigQuery

BigQuery - GA4 Export Queries

BigQuery SQL · Same-session join

Search to Detail: Same-Session Timing and Rate

Returns one row per search session, pairing its first search with its first later commodity-detail view. Unmatched search sessions remain in the denominator.

SQL: BigQuery daily export
WITH extracted AS (
  SELECT
    user_pseudo_id,
    event_name,
    event_timestamp,
    (SELECT value.int_value FROM UNNEST(event_params)
     WHERE key = 'ga_session_id') AS session_id,
    (SELECT value.string_value FROM UNNEST(event_params)
     WHERE key = 'page_location') AS page_location
  FROM `your-project.analytics_XXXXXX.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260131'
),
events AS (
  SELECT * FROM extracted
  WHERE user_pseudo_id IS NOT NULL AND session_id IS NOT NULL
),
searches AS (
  SELECT user_pseudo_id, session_id, MIN(event_timestamp) AS search_ts
  FROM events
  WHERE event_name = 'view_search_results'
  GROUP BY 1, 2
),
matched AS (
  SELECT
    s.user_pseudo_id, s.session_id, s.search_ts,
    MIN(e.event_timestamp) AS commodity_ts
  FROM searches s
  LEFT JOIN events e
    ON e.user_pseudo_id = s.user_pseudo_id
    AND e.session_id = s.session_id
    AND e.event_name = 'page_view'
    AND e.event_timestamp > s.search_ts
    AND REGEXP_CONTAINS(e.page_location,
      r'^https?://[^/]+/commodities/[0-9]{10}([/?#].*)?$')
  GROUP BY 1, 2, 3
)
SELECT
  user_pseudo_id, session_id,
  TIMESTAMP_MICROS(search_ts) AS search_timestamp,
  TIMESTAMP_MICROS(commodity_ts) AS commodity_timestamp,
  1 AS has_search,
  IF(commodity_ts IS NOT NULL, 1, 0) AS has_commodity_after_search,
  (commodity_ts - search_ts) / 1000000.0 AS seconds_to_commodity
FROM matched;
Boundaries: Replace the dataset and dates. The query uses user_pseudo_id AND ga_session_id, and filters both stages to the same date range. It excludes missing identifiers and equal timestamps. It allows intervening events and uses the first search per session; it does not pair each search attempt separately. Sessions crossing the date boundary may be truncated. Anonymous export counts can differ from GA4 reporting identity and modelling.
BigQuerywindow-functionevent-sequencetime-between
BigQuery SQL · UNNEST

Extract Custom Event Parameters with UNNEST

The standard pattern for reading event_params from GA4's BigQuery export - works for any custom dimension like click_text, click_classes, or data-tree-target values.

SQL: BigQuery event parameters
SELECT
  event_name,
  user_pseudo_id,
  TIMESTAMP_MICROS(event_timestamp) AS event_ts,
  (SELECT value.string_value FROM UNNEST(event_params)
   WHERE key = 'click_text') AS click_text,
  (SELECT value.string_value FROM UNNEST(event_params)
   WHERE key = 'page_location') AS page_location,
  (SELECT COALESCE(CAST(value.int_value AS FLOAT64),
                   value.double_value, value.float_value)
   FROM UNNEST(event_params)
   WHERE key = 'value') AS event_value
FROM `your-project.analytics_XXXXXX.events_*`
WHERE event_name = 'internal_link_click'
  AND _TABLE_SUFFIX BETWEEN '20260101' AND '20260131'
LIMIT 1000;
Types: Inspect actual exported value types. Numeric values can use int_value or double_value; float_value exists in the schema but is not currently used. DebugView can confirm collection but does not establish which BigQuery storage column contains a value. This example assumes parameter keys are unique per event.
BigQueryUNNESTevent_paramsclick_text
Looker Studio

Common Metrics & Dimensions

Looker Studio Metric · Engagement

Bounce Rate (GA4-native)

GA4 defines bounce rate as the inverse of engagement rate. Two equivalent formulas - use whichever fields are available in your data source.

Calculated field: check input names and types
1 - Engagement rate
Raw counts: Percent
CASE
  WHEN SUM(session_count) > 0
    THEN (SUM(session_count) - SUM(engaged_session_count)) / SUM(session_count)
  ELSE NULL
END
Format: Percent, assuming Engagement rate is a fraction between 0 and 1. An engaged session meets the configured duration threshold, contains a qualifying key event, or has at least two page/screen views. The threshold is configurable; do not assume it is always 10 seconds. For raw session counts, use the guarded alternative below.
bounce-rateengagement-ratemetric
Looker Studio CASE · Channel Grouping

Custom Channel Grouping

Override GA4's default channel labels or collapse them into your own taxonomy. Useful for combining brand/non-brand paid, grouping social platforms, or renaming channels to match internal reporting language.

Calculated field: check input names and types
CASE
  WHEN Session default channel group = "Paid Search"
    AND REGEXP_MATCH(Session campaign, "(?i)^brand([_ -].*)?$")
    THEN "Paid Search - Brand"
  WHEN Session default channel group = "Paid Search"
    THEN "Paid Search - Non-Brand"
  WHEN Session default channel group = "Paid Social"
    AND REGEXP_MATCH(Session source, "(?i).*(facebook|instagram|meta).*")
    THEN "Meta Ads"
  WHEN Session default channel group = "Paid Social"
    AND REGEXP_MATCH(Session source, "(?i).*linkedin.*")
    THEN "LinkedIn Ads"
  WHEN Session default channel group = "Organic Social" THEN "Organic Social"
  ELSE Session default channel group
END
Campaign convention: Rename campaigns consistently, for example brand_uk and nonbrand_uk. A naive .*brand.* rule also matches nonbrand. These labels only change this report; they do not edit GA4 channel definitions. Use the actual field names available in your connector.
channel-groupingCASEsource-mediumpaid-search
Looker Studio CASE · Device

Device + OS Combined Dimension

Creates a single dimension combining device category and operating system - more granular than device alone, useful for spotting iOS vs Android performance gaps on mobile.

Calculated field: check input names and types
CASE
  WHEN Device category = "desktop" AND REGEXP_MATCH(Operating system, "(?i).*windows.*") THEN "Desktop - Windows"
  WHEN Device category = "desktop" AND REGEXP_MATCH(Operating system, "(?i).*mac.*") THEN "Desktop - Mac"
  WHEN Device category = "desktop" THEN "Desktop - Other"
  WHEN Device category = "mobile" AND REGEXP_MATCH(Operating system, "(?i).*ios.*") THEN "Mobile - iOS"
  WHEN Device category = "mobile" AND REGEXP_MATCH(Operating system, "(?i).*android.*") THEN "Mobile - Android"
  WHEN Device category = "mobile" THEN "Mobile - Other"
  WHEN Device category = "tablet" THEN "Tablet"
  ELSE "Unknown"
END
Simple device grouping
CASE
  WHEN Device category = "mobile" THEN "Mobile"
  WHEN Device category = "desktop" OR Device category = "tablet" THEN "Desktop / Tablet"
  ELSE "Unknown"
END
Scope: These are report labels for device and OS values. Use the separate code block for the simpler grouping. Unexpected device values remain Unknown rather than being assigned to Desktop / Tablet.
devicemobileOSbucketing
Looker Studio REGEXP_REPLACE · URL Cleaning

Clean Landing Page - Strip Query Strings

Strips query strings and fragments from a path or full URL. Use the query-string version of the landing-page field when available; a path-only field needs no cleaning.

Calculated field: check input names and types
REGEXP_REPLACE(Landing page + query string, "[?#].*$", "")
Full URL input
REGEXP_EXTRACT(Page location, "^https?://[^/?#]+(/[^?#]*)")
Choice: Use the actual field in your connector. GA4's Landing page field is normally path-only; Landing page + query string includes parameters. Do not discard query parameters that define meaningful content differences. The alternative below extracts a clean path from a full URL and assumes that a slash follows the hostname.
landing-pageREGEXP_REPLACEurl-cleaningUTM
Looker Studio CASE · User Type

Observed Session Number Labels

Labels a session-level ga_session_number value collected or extracted from your source. Aggregate Sessions, New users and Returning users metrics cannot recover an individual session number.

Calculated field: modelled source
CASE
  WHEN session_number = 1 THEN "First observed session"
  WHEN session_number = 2 THEN "Second observed session"
  WHEN session_number >= 3 AND session_number <= 5 THEN "Sessions 3 to 5"
  WHEN session_number >= 6 THEN "Session 6 or later"
  ELSE "Unknown"
END
Source: Extract ga_session_number from event_params in BigQuery, then model at session grain. Cookie resets, consent and device changes can restart observed history. This is not a lifetime loyalty label and is not a field guaranteed by the native Looker Studio GA4 connector.
new-usersreturning-usersCASEuser-type
Looker Studio Metric · Paid Media

ROAS, CPA & Revenue-per-Session

Calculate each metric separately using compatible revenue, cost and outcome scopes, currencies and date ranges. Linking Google Ads is not a prerequisite for revenue per session or revenue per user.

Calculated field: modelled source
CASE
  WHEN SUM(ad_cost) > 0 THEN SUM(attributed_purchase_revenue) / SUM(ad_cost)
  ELSE NULL
END
CPA
CASE WHEN SUM(acquisitions) > 0 THEN SUM(ad_cost) / SUM(acquisitions) ELSE NULL END
Revenue per session
CASE WHEN SUM(session_count) > 0 THEN SUM(purchase_revenue) / SUM(session_count) ELSE NULL END
Revenue per user
CASE WHEN SUM(user_count) > 0 THEN SUM(purchase_revenue) / SUM(user_count) ELSE NULL END
Gross margin at 40% COGS
CASE WHEN SUM(purchase_revenue) > 0 THEN (SUM(purchase_revenue) - SUM(purchase_revenue) * 0.4) / SUM(purchase_revenue) ELSE NULL END
Source: The examples use raw pre-aggregated numeric fields, not promised native connector names. Define acquisition precisely: purchases, leads, or another selected outcome. Match attribution and campaign scope before blending cost and revenue. Format ROAS as Number, CPA as Currency, and margin as Percent. The final expression is gross margin under a fixed cost-of-goods assumption, not net profit.
ROASCPApaid-mediarevenue
Looker Studio CASE · Scroll Depth

Scroll Depth Bucketing

Buckets a numeric percent_scrolled parameter from custom scroll_depth events. GA4 enhanced measurement alone provides the 90% threshold, not the full set of depth tiers.

Calculated field: check input names and types
CASE
  WHEN percent_scrolled IS NULL THEN "Unknown"
  WHEN CAST(percent_scrolled AS NUMBER) >= 90 THEN "4 - 90% or more"
  WHEN CAST(percent_scrolled AS NUMBER) >= 75 THEN "3 - 75% to 89%"
  WHEN CAST(percent_scrolled AS NUMBER) >= 50 THEN "2 - 50% to 74%"
  WHEN CAST(percent_scrolled AS NUMBER) >= 25 THEN "1 - 25% to 49%"
  ELSE "0 - Below 25%"
END
Setup: Collect custom thresholds with GTM and filter Event name to scroll_depth, matching the configuration below. A threshold event is not a user's maximum depth: one visit can contribute several events. Pre-aggregate maximum depth per page visit for mutually exclusive visit-level buckets. Missing scroll events do not prove shallow engagement; validate numeric values before casting.
scroll-depthengagementCASEpercent_scrolled
Looker Studio REGEXP_MATCH · Outbound

Outbound vs Internal Link Classification

Labels clicks as outbound or internal based on whether the link URL matches your own domain - useful for outbound click analysis without needing a separate event for each external destination.

Calculated field: check input names and types
CASE
  WHEN Link URL IS NULL OR Link URL = "" THEN "No URL"
  WHEN REGEXP_MATCH(Link URL, "(?i)^https?://(?:[^/?#:]+[.])*yourdomain[.]com(?::[0-9]+)?(?:[/?#].*)?$") THEN "Internal"
  WHEN REGEXP_MATCH(Link URL, "(?i)^https?://.*") THEN "Outbound"
  ELSE "Other URL type"
END
Destination hostname
REGEXP_EXTRACT(Link URL, "(?i)^https?://(?:www[.])?([^/?#:]+)")
Domain: Replace yourdomain[.]com with your domain, escaping dots with [.] and adapting any other special characters. This classifies absolute HTTP(S) URLs and their subdomains, not relative links. Enhanced measurement's click event captures outbound links; it does not populate a complete internal-click dataset. Collect internal clicks separately and send the destination URL. Configured cross-domain destinations may be excluded from enhanced outbound tracking.
outboundlink-clickdomainREGEXP_MATCH
Looker Studio Metric · Engagement

Derived Engagement Metrics

Derived ratios from compatible raw totals. These examples do not reconstruct Universal Analytics definitions or infer active attention from elapsed session duration.

Calculated field: modelled source
CASE WHEN SUM(session_count) > 0 THEN SUM(view_count) / SUM(session_count) ELSE NULL END
Views per active user
CASE WHEN SUM(active_user_count) > 0 THEN SUM(view_count) / SUM(active_user_count) ELSE NULL END
Engaged sessions per active user
CASE WHEN SUM(active_user_count) > 0 THEN SUM(engaged_session_count) / SUM(active_user_count) ELSE NULL END
Engagement seconds per session
CASE WHEN SUM(session_count) > 0 THEN SUM(engagement_seconds) / SUM(session_count) ELSE NULL END
Units: Each field is a raw numeric total at compatible grain. Format views per session as Number. Keep time results in seconds for Duration formatting; do not divide by 60 and then format as seconds. If you need numeric minutes, divide seconds by 60 and format as Number. Engagement time and elapsed session duration measure different things.
views-per-sessionengagementsession-durationmetric
GTM

GTM - More Useful Variables

GTM Scroll Depth Trigger

Multi-Threshold Scroll Depth Tracking

GTM trigger + GA4 event setup to fire scroll events at 25%, 50%, 75%, and 90% - giving you full depth data rather than GA4's default 90%-only enhanced measurement.

Configuration / reference steps
-- Step 1: Create Scroll Depth Trigger
Trigger type      : Scroll Depth
Vertical depths   : ☑ Percentages → 25, 50, 75, 90
Evaluate trigger  : Window Load (adapt and test for your page)
Enable trigger on : All Pages, or a defined subset

-- Step 2: Enable built-in variables
☑ Scroll Depth Threshold   (returns 25, 50, 75, 90)
☑ Scroll Depth Units       (returns "percent")

-- Step 3: GA4 Event Tag
Event name: scroll_depth
Event parameters:
  percent_scrolled  →  {{Scroll Depth Threshold}}
  scroll_units      →  {{Scroll Depth Units}}

-- Step 4: Register in GA4
Custom dimension: percent_scrolled (Event scope)
Custom dimension: scroll_units     (Event scope)

-- Test in Preview mode: scroll the page and check
-- each threshold fires once per page load
Interpretation: Use scroll_depth for custom thresholds so it stays distinct from the enhanced-measurement scroll event. Select an appropriate trigger evaluation point for your page and test dynamic content. Register percent_scrolled and scroll_units if needed for reporting. The collected threshold is numeric; a custom-dimension connector may expose it as text. Do not assume every scroll count is a distinct visitor.
scroll-depthtriggerGTMenhanced-measurement
GTM Custom JavaScript Variable

Useful URL Utility Variables

URL utilities for Custom JavaScript variables. Prefer GTM's built-in URL variables for standard URL components; use custom JavaScript when it adds a specific behaviour.

JavaScript: GTM variable
function() { return window.location.pathname; }
Query parameter
function() {
  return new URL(window.location.href).searchParams.get('utm_campaign');
}
Hostname
function() { return window.location.hostname; }
Hash
function() { return window.location.hash || undefined; }
URL without query or fragment
function() { return window.location.origin + window.location.pathname; }
Setup: Create a separate variable for each code block. The query-parameter example uses browser URL APIs; verify support in your target browsers. Built-in URL variables can also extract query parameters without custom code.
URLhostnamequery-paramcustom-js
GTM Custom JavaScript Variable

Read a First-Party Cookie

Reads a JavaScript-accessible cookie by exact name. Cookie presence does not prove authenticated status, consent or assignment to the control variant.

JavaScript: GTM variable
function() {
  var name = 'your_cookie_name';
  var cookies = document.cookie ? document.cookie.split(';') : [];
  for (var i = 0; i < cookies.length; i++) {
    var item = cookies[i].replace(/^\s+/, '');
    var equals = item.indexOf('=');
    if (equals !== -1 && item.slice(0, equals) === name) {
      var value = item.slice(equals + 1);
      try { return decodeURIComponent(value); }
      catch (e) { return value; }
    }
  }
  return undefined;
}
Limits: HttpOnly cookies are inaccessible to JavaScript. Use the application's explicit data-layer status for authentication, the CMP's supported consent integration for consent and an explicit experiment assignment for variants. Treat missing assignment as Unknown or Unassigned, never automatically Control. Do not send cookie identifiers or secrets to GA4.
cookieuser-statusA/B-testcustom-js
BigQuery

BigQuery - Funnel & Session Queries

BigQuery SQL · Funnel Analysis

Closed Same-Session Funnel: Ordered Steps

Counts sessions progressing through view_item, add_to_cart, begin_checkout and purchase in order. Each session contributes at most once per stage.

SQL: BigQuery daily export
WITH extracted AS (
  SELECT
    user_pseudo_id,
    event_name,
    event_timestamp,
    (SELECT value.int_value FROM UNNEST(event_params)
     WHERE key = 'ga_session_id') AS session_id,
    (SELECT value.string_value FROM UNNEST(event_params)
     WHERE key = 'page_location') AS page_location
  FROM `your-project.analytics_XXXXXX.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260131'
),
events AS (
  SELECT * FROM extracted
  WHERE user_pseudo_id IS NOT NULL AND session_id IS NOT NULL
),
views AS (
  SELECT user_pseudo_id, session_id, MIN(event_timestamp) AS view_ts
  FROM events WHERE event_name = 'view_item'
  GROUP BY 1, 2
),
carts AS (
  SELECT v.user_pseudo_id, v.session_id, v.view_ts,
         MIN(e.event_timestamp) AS cart_ts
  FROM views v LEFT JOIN events e
    ON e.user_pseudo_id = v.user_pseudo_id AND e.session_id = v.session_id
    AND e.event_name = 'add_to_cart' AND e.event_timestamp > v.view_ts
  GROUP BY 1, 2, 3
),
checkouts AS (
  SELECT c.user_pseudo_id, c.session_id, c.view_ts, c.cart_ts,
         MIN(e.event_timestamp) AS checkout_ts
  FROM carts c LEFT JOIN events e
    ON e.user_pseudo_id = c.user_pseudo_id AND e.session_id = c.session_id
    AND e.event_name = 'begin_checkout' AND e.event_timestamp > c.cart_ts
  GROUP BY 1, 2, 3, 4
),
purchases AS (
  SELECT c.user_pseudo_id, c.session_id, c.view_ts, c.cart_ts, c.checkout_ts,
         MIN(e.event_timestamp) AS purchase_ts
  FROM checkouts c LEFT JOIN events e
    ON e.user_pseudo_id = c.user_pseudo_id AND e.session_id = c.session_id
    AND e.event_name = 'purchase' AND e.event_timestamp > c.checkout_ts
  GROUP BY 1, 2, 3, 4, 5
),
counts AS (
  SELECT COUNT(*) AS viewed,
         COUNTIF(cart_ts IS NOT NULL) AS carted,
         COUNTIF(checkout_ts IS NOT NULL) AS checked_out,
         COUNTIF(purchase_ts IS NOT NULL) AS purchased
  FROM purchases
),
steps AS (
  SELECT 1 AS step_number, 'View item' AS step, viewed AS sessions,
         CAST(NULL AS INT64) AS previous_sessions FROM counts
  UNION ALL SELECT 2, 'Add to cart', carted, viewed FROM counts
  UNION ALL SELECT 3, 'Checkout', checked_out, carted FROM counts
  UNION ALL SELECT 4, 'Purchase', purchased, checked_out FROM counts
)
SELECT *, SAFE_DIVIDE(sessions, previous_sessions) AS step_rate,
       1 - SAFE_DIVIDE(sessions, previous_sessions) AS drop_off_rate
FROM steps ORDER BY step_number;
Definition: This is a closed session funnel with intervening events allowed and strictly increasing timestamps. It measures session progression, not individual-item continuity or user conversion. It excludes missing identifiers and truncates sessions at date boundaries. Events sharing a timestamp require additional ordering metadata; this example excludes them. Format rates as Percent.
funneldrop-offconversion-rateecommerce
BigQuery SQL · User Metrics

Daily Active Users + 7-Day Rolling Average

Produces a daily user count time series with a 7-day rolling average to smooth noise - ideal for CMO-level trend charts in Looker Studio.

SQL: BigQuery daily export
WITH daily AS (
  SELECT PARSE_DATE('%Y%m%d', event_date) AS date,
         COUNT(DISTINCT IF(is_active_user, user_pseudo_id, NULL)) AS dau
  FROM `your-project.analytics_XXXXXX.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260131'
  GROUP BY 1
),
calendar AS (
  SELECT date FROM UNNEST(
    GENERATE_DATE_ARRAY(DATE '2026-01-01', DATE '2026-01-31')
  ) AS date
),
filled AS (
  SELECT c.date, COALESCE(d.dau, 0) AS dau
  FROM calendar c LEFT JOIN daily d USING (date)
)
SELECT date, dau,
       IF(COUNT(*) OVER w = 7, AVG(dau) OVER w, NULL) AS dau_7day_avg
FROM filled
WINDOW w AS (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
ORDER BY date;
Definition: Uses is_active_user from completed daily export tables, not intraday tables, and fills missing dates before the rolling average. The first six rows have no full seven-day average; include six prior dates if you need an average from the first displayed day. A missing export table is not proof of zero activity: check export completeness. The exported pseudonymous active-user count can differ from GA4 reporting identity and modelling.
DAUrolling-averagetime-serieswindow-function