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_idto 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
dataLayerand 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.
Navigation and discovery
select_content values are built in ga4SelectContent.ts. The entries below
are pushed by the application; click-programa is left to GTM (see
isBuiltByGTM).
| Event | content_type / params | Source |
|---|---|---|
select_content | menu-lateral:{texto} | Item clicked inside the open side menu (ClientMenu); top-bar items keep header:menu:{texto} |
select_content | menu-lateral:switch-profile | Side-menu profile entry (ClientMenu) |
select_content | click-categoria:{categoria} | Category card in a Category rail or grid, opening /category/{id} |
select_content | programacao-local:filtro-estado + estado | Your Region state selector; estado is the region slug |
select_content | opcoes-serie:{episodios|sugeridos|detalhes} + id_programa_click, programa_click, categoria_click | Programme page tabs (DetailBottomSection) |
reproduzir | id_programa_click, programa_click, categoria_click | Content 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.
| Event | content_type / params | Source |
|---|---|---|
select_content | adicionar-perfil | Add-profile tile on /profile and /profile/edit (ProfileSelectorList) |
select_content | editar-perfis | "Manage profiles" link (ProfileBottomButtons) |
select_content | criar-perfil:{continuar|cancelar} + perfil_infantil (on|off) | Create-profile form Continue / Cancel (ProfileCreator); not the Back arrow |
select_content | controle-parental-salvar + classificacao | Parental-control Save in every flow (ParentalRatingScreen); see below |
select_content | editar-perfil:{criar-pin|pin-do-perfil|controle-parental} | Edit-profile rows (ProfileEditForm); pin-do-perfil (PIN row when a PIN exists) is assumed |
select_content | criar-pin:{continuar|cancelar} | Create-PIN first step (ProfileManagePin), including the forgot-PIN create step (assumed) |
select_content | confirmar-pin:{continuar|cancelar} | Create-PIN confirm step (ProfileManagePin), including the forgot-PIN confirm step (assumed) |
select_content | pin-do-perfil:{alterar-pin|remover-pin} | Profile PIN settings rows (ProfileEditPin); slugs of the clicked labels |
select_content | alterar-pin:{continuar|esqueci-o-pin} | Change PIN (ProfileManagePin); continuar on the current-PIN step only (assumed) |
select_content | excluir-pin:{continuar|esqueci-o-pin} | Remove PIN (ProfileManagePin) |
select_content | confirmar-exclusao-pin:{confirmar|cancelar} | Remove-PIN confirmation dialog buttons (ProfileEditPin) |
select_content | entre-com-sua-senha:{continue|esqueci-a-senha} | Enter-password Continue button or Enter key, and forgot-password link (ProfileEnterPassword) |
select_content | configuracao-da-conta:{minha-conta|assinatura} | Account settings list click (SplitScreen via AccountPageSection); not on initial render/deep link |
select_content | assinatura:{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.
| Event | content_type | Source |
|---|---|---|
select_content | assinar:{nome do plano} | Plan-screen commit button (PlanSelection) on /subscription and /account/subscription/select; slug of plan.title |
select_content | escolha-seu-plano:sair | Sair 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_content | header:logo | Progress-menu logo on /subscription (ClientProgressMenu with isPlanScreen); the main menu logo pushes it anywhere |
select_content | pacote-escolhido:voltar | Voltar 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_progressat 25, 50 and 75 percent withvideo_percent, andvideo_complete. - Live and radio:
video_start, thenlive_progressevery 300 seconds of active watch time (Web map v3, section 12.4).live_progresscarries the livepage_viewidentifiers (id_canal,canal,formato) plusvideo_current_time,watch_time_secondsandaudit_timestamp, and nevervideo_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, alive_progresscarries 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 livevideo_progress(ACS-5237). GTM needs alive_progresstrigger 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
- Open browser DevTools and inspect
window.dataLayer. - Perform the mapped action and verify exactly one object with the expected
eventand parameters. - For ecommerce, verify
{ ecommerce: null }immediately precedes the event. - In GTM Preview, confirm the matching Record-owned tag fires once.
- 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.