Skip to main content

GA4 / i-cherry Web dataLayer

Ownership​

Record Web emits the client-approved analytics contract as plain objects in window.dataLayer and loads the client-owned Google Tag Manager container from NEXT_PUBLIC_GTM_ID. Record owns the container configuration that maps those objects to GA4/i-cherry. The application does not configure a GA4 measurement ID, call gtag() or send events directly to a GA4 property.

The contract is the Web v3 tracking plan (GA4 Tags Map v3.pdf, "RECORD PlayPlus (Web) Mapa de Tags GA4", SHA-256 8476f5e90ff9b8922a764fe6b406df9c57527dfa6599383cf53b4e005027b97e). It supersedes Web v09 and the 2026-08-04 Accedo-layout revision ([R7+iC] Mapa de Tags GA4 - RecordPlus - Web-v1.pdf, SHA-256 30165e59acf11a720e2a7343d644046e572d3548189556665f57be4392042825). The v3 screens come from the legacy site, so each one is mapped to the equivalent Record Web screen.

Event shape​

Emitters call trackMarketingEvent / trackMarketingEcommerceEvent, which push the dataLayer objects below and send the same event to Permutive when it is configured (see Permutive).

Non-ecommerce events use a flat dataLayer object:

globalThis.dataLayer.push({ event: 'login', method: 'email' });

Ecommerce events clear stale ecommerce state and then use the documented nested shape:

globalThis.dataLayer.push({ ecommerce: null });
globalThis.dataLayer.push({
event: 'purchase',
ecommerce: {
currency: 'BRL',
value: 30.5,
transaction_id: '...',
items: [],
},
});

Authenticated identity is dataLayer state. user_id is the ArcXP account UUID exposed by the authenticated session as user.id; email is never used for GA4 identity. plano is the active plan title and planoid is its ID. All three values are cleared on logout. client_id is generated by GA4 and is not sent as a custom field.

Personal data — sign-off required​

user_id is a stable pseudonymous account identifier and remains personal data under the LGPD. Record confirmed the ArcXP UUID contract in ACS-5149. The application exposes it only after an Adult profile enables commercial measurement; pre-profile product measurement remains anonymous and Kids measurement remains blocked.

Before this ships to production, record here:

  • Legal basis / consent — which basis covers pushing user_id to Record-owned GTM, and whether it must be gated on a consent signal rather than emitted for every authenticated session.
  • Retention — how long the GA4 property keeps user-scoped data.
  • Deletion path — how an LGPD erasure request reaches GA4, given the application only writes to dataLayer and does not own the property.

The application's runtime policy clears the identity fields whenever commercial measurement is unavailable or the authenticated session lacks an ArcXP UUID.

Status: unresolved — needs Record's DPO/legal confirmation.

Page views​

useGA4ScreenTracking(pathname) emits one explicit page_view for each route change. ga4ScreenPaths.ts maps the Next.js pathname to the canonical i-cherry page_title; page_location remains the real browser URL.

select_content values are built in ga4SelectContent.ts. The entries below are pushed by the application; click-programa is left to GTM (see isBuiltByGTM).

Eventcontent_type / paramsSource
select_contentmenu-lateral:{texto}Item clicked inside the open side menu (ClientMenu); top-bar items keep header:menu:{texto}
select_contentmenu-lateral:switch-profileSide-menu profile entry (ClientMenu)
select_contentclick-categoria:{categoria}Category card in a Category rail or grid, opening /category/{id}
select_contentprogramacao-local:filtro-estado + estadoYour Region state selector; estado is the region slug
select_contentopcoes-serie:{episodios|sugeridos|detalhes} + id_programa_click, programa_click, categoria_clickProgramme page tabs (DetailBottomSection)
reproduzirid_programa_click, programa_click, categoria_clickContent PLAY CTA (CTAButtons); not the trailer CTA

FAQ (faq:*) and contact (contato:redes-sociais:*) are not emitted: the FAQ is the external descubra.recordplus.com/faq/ site, reached from the footer and tracked as footer:{opção}, and Record Web has no contact screen.

Profile and account​

Pushed by the application on the click of the mapped option, before any validation or API call. Back arrows and dialog dismissal send nothing. Values marked assumed are not in the Web v3 tracking plan and await confirmation.

Eventcontent_type / paramsSource
select_contentadicionar-perfilAdd-profile tile on /profile and /profile/edit (ProfileSelectorList)
select_contenteditar-perfis"Manage profiles" link (ProfileBottomButtons)
select_contentcriar-perfil:{continuar|cancelar} + perfil_infantil (on|off)Create-profile form Continue / Cancel (ProfileCreator); not the Back arrow
select_contentcontrole-parental-salvar + classificacaoParental-control Save in every flow (ParentalRatingScreen); see below
select_contenteditar-perfil:{criar-pin|pin-do-perfil|controle-parental}Edit-profile rows (ProfileEditForm); pin-do-perfil (PIN row when a PIN exists) is assumed
select_contentcriar-pin:{continuar|cancelar}Create-PIN first step (ProfileManagePin), including the forgot-PIN create step (assumed)
select_contentconfirmar-pin:{continuar|cancelar}Create-PIN confirm step (ProfileManagePin), including the forgot-PIN confirm step (assumed)
select_contentpin-do-perfil:{alterar-pin|remover-pin}Profile PIN settings rows (ProfileEditPin); slugs of the clicked labels
select_contentalterar-pin:{continuar|esqueci-o-pin}Change PIN (ProfileManagePin); continuar on the current-PIN step only (assumed)
select_contentexcluir-pin:{continuar|esqueci-o-pin}Remove PIN (ProfileManagePin)
select_contentconfirmar-exclusao-pin:{confirmar|cancelar}Remove-PIN confirmation dialog buttons (ProfileEditPin)
select_contententre-com-sua-senha:{continue|esqueci-a-senha}Enter-password Continue button or Enter key, and forgot-password link (ProfileEnterPassword)
select_contentconfiguracao-da-conta:{minha-conta|assinatura}Account settings list click (SplitScreen via AccountPageSection); not on initial render/deep link
select_contentassinatura:{fazer-upgrade-do-plano|alterar-plano|gerenciar-metodo-de-pagamento}Subscription plan action and manage payment (AccountSubscription); alterar-plano is assumed

classificacao is looked up from the configured rating.value, never the CMS name: livre → livre, 6_anos → ate-seis, 10_anos → ate-dez, 12_anos → ate-doze, 14_anos → ate-catorze, 16_anos → ate-dezesseis, 18_anos → ate-dezoito. An unknown rating pushes classificacao: undefined so a value from an earlier save in the same page doesn't carry over in the GTM data model. Both perfil_infantil and classificacao reach GA4 only once the GTM tag maps them.

perfil_infantil is only pushed with criar-perfil:* and persists in the GTM data model after that push, so the GTM mapping should read it only on criar-perfil:* triggers.

The tracking plan's §6.2.7 menu-perfil:{opção} is not emitted: Record Web has no such menu, and the side menu already sends menu-lateral:*. The profile-switch "Enter PIN" prompt is not emitted until its button values are confirmed. Sair in the side menu sends menu-lateral:sair (except on the plan screen, below) and logout clears user_id.

Subscription funnel​

select_content values from the Web v3 "Escolha o seu plano" screens, pushed by the application on the click of the mapped control, before navigation or logout.

Eventcontent_typeSource
select_contentassinar:{nome do plano}Plan-screen commit button (PlanSelection) on /subscription and /account/subscription/select; slug of plan.title
select_contentescolha-seu-plano:sairSair on the plan screen: progress menu on /subscription (ClientProgressMenu with isPlanScreen), main-menu logout on /account/subscription/select (ClientMenu), pushed on click before the logout confirmation
select_contentheader:logoProgress-menu logo on /subscription (ClientProgressMenu with isPlanScreen); the main menu logo pushes it anywhere
select_contentpacote-escolhido:voltarVoltar button on the billing screen (BillingForm), before history.back(); browser Back sends nothing

The progress menu is shared with the signup, billing, payment and account screens, which send neither progress-menu value. /account/subscription/select renders the main menu, whose logo already pushes header:logo on every page; its logout pushes escolha-seu-plano:sair on that route instead of menu-lateral:sair.

Player engagement​

GA4PlayerEngagementEngine measures active watch time (pauses, buffering, seeks and ads suspend the clock). video_start fires as soon as content starts playing (Web map v3), never during a pre-roll, and waits until the series title or the live EPG programme has loaded so the whole session carries one programa. programa is the series title for VOD and the EPG programme on air for live, where the channel goes in id_canal/canal (src/features/Player/ga4PlayerContent.ts). It emits:

  • VOD: video_start, video_progress at 25, 50 and 75 percent with video_percent, and video_complete.
  • Live and radio: video_start, then live_progress every 300 seconds of active watch time (Web map v3, section 12.4). live_progress carries the live page_view identifiers (id_canal, canal, formato) plus video_current_time, watch_time_seconds and audit_timestamp, and never video_percent. When the live session ends (player exit, channel change, stream end or page hide), and when the EPG rolls over to the next programme on the same channel, a live_progress carries the watch time since the last heartbeat, so sessions shorter than 5 minutes still report their viewing time (i-Cherry 120 second qualified view). It replaced the earlier 60 second live video_progress (ACS-5237). GTM needs a live_progress trigger and tag before the event reaches GA4.

Every player event also carries categoria: the first CMS category for VOD, and the on-air programme's editorial genre for live (EPG genres, else categories). VOD events add conteudo (the OVP contentType, else the item type, lowercased), duracao (seconds, rounded), next to video_duration_seconds, and data_de_lancamento. The catalog only has a release year, so data_de_lancamento is the year (2024), not the YYYY-MM-DD date of the v6 map, and it is left out when the year is missing.

The player page_view for channels, radio and VOD comes from the Player, not the server page, so it carries the same identity as the engagement events. It is sent once per media, after the series title or the programme on air has loaded:

  • Live and radio: id_canal, canal, id_programa, programa, formato. The programme fields are left out when the EPG has no data.
  • VOD: id_programa (the show for an episode), content_id, programa, titulo_do_video, categoria, conteudo, duracao (seconds), data_de_lancamento (year), formato.

If the EPG never returns the channel, the Player stops waiting after 3 seconds and sends the channel-only page_view and video_start. Windowed live events (event/eventstream) keep the program page_view rendered by the player page.

Payment information​

trackAddPaymentInfo implements the approved add_payment_info payload, including payment_type, but is intentionally not connected to the current Cleeng Hosted Widget. That API exposes checkout loaded and payment result events, not payment-method selection. Emitting it at payment success would put the event at the wrong funnel stage. Connect it only when the checkout provides a genuine pre-purchase payment-method callback.

Validation​

  1. Open browser DevTools and inspect window.dataLayer.
  2. Perform the mapped action and verify exactly one object with the expected event and parameters.
  3. For ecommerce, verify { ecommerce: null } immediately precedes the event.
  4. In GTM Preview, confirm the matching Record-owned tag fires once.
  5. In GA4 DebugView/Realtime, filter by the test build, device and timestamp and verify the event name and parameters. Do not use unrelated legacy-app traffic as evidence for this implementation.

The deployment workflow requires the public GitHub environment variable GTM_CONTAINER_ID. The application validates that it matches the GTM-* web container format and renders no GTM markup for invalid values. The client must publish a container version whose triggers and variables consume this documented dataLayer contract; loading the container alone does not validate that mapping.