conversion_label(); $now = (float) Wording::val( $d, array( 'now' ) ); $prev = (float) Wording::val( $d, array( 'prev' ) ); $sessions = isset( $d['sessions'] ) && is_array( $d['sessions'] ) ? $d['sessions'] : null; $events = isset( $d['events'] ) ? (array) $d['events'] : array(); $pages = isset( $d['pages'] ) ? (array) $d['pages'] : array(); $lost = max( 1, $prev - $now ); $facts = array( Wording::fact( $label, $prev, $now ) ); $metrics = array( Wording::metric( $label, Wording::num( $now ), Rules::change( $now, $prev ) ) ); $insights = array( sprintf( /* translators: 1: conversion label, 2: percentage, 3: before, 4: now. */ __( '%1$s from organic search fell %2$s (%3$s → %4$s).', 'turtlereport' ), $label, Wording::pct_abs( Wording::val( $d, array( 'change' ) ) ), Wording::num( $prev ), Wording::num( $now ) ), ); $pattern = 'mixed'; if ( $sessions ) { list( $s_now, $s_prev ) = array( (float) $sessions[0], (float) $sessions[1] ); $cr_now = $s_now > 0 ? $now / $s_now : 0; $cr_prev = $s_prev > 0 ? $prev / $s_prev : 0; $sc = Rules::change( $s_now, $s_prev ); $crc = Rules::change( $cr_now, $cr_prev ); $facts[] = Wording::fact( __( 'Organic sessions', 'turtlereport' ), $s_prev, $s_now ); $facts[] = Wording::fact( __( 'Conversion rate', 'turtlereport' ), $cr_prev, $cr_now, 'ratio' ); $metrics[] = Wording::metric( __( 'Organic sessions', 'turtlereport' ), Wording::num( $s_now ), $sc ); $metrics[] = array( 'label' => __( 'Conversion rate', 'turtlereport' ), 'value' => Wording::pct( $cr_now ), 'change' => Wording::pts( ( $cr_now - $cr_prev ) * 100 ), 'tone' => abs( $cr_now - $cr_prev ) * 100 < 0.1 ? 'flat' : ( $cr_now < $cr_prev ? 'bad' : 'good' ), ); if ( null !== $sc && $sc <= -15 && ( null === $crc || $crc > -20 ) ) { $pattern = 'traffic'; $insights[] = sprintf( /* translators: 1: sessions before, 2: sessions now, 3: rate before, 4: rate now. */ __( 'Organic visits fell by a similar amount (%1$s → %2$s) while the conversion rate held (%3$s → %4$s): fewer people arrived, rather than fewer of them converting. The cause is upstream, in search traffic.', 'turtlereport' ), Wording::num( $s_prev ), Wording::num( $s_now ), Wording::pct( $cr_prev ), Wording::pct( $cr_now ) ); } elseif ( null !== $sc && $sc > -10 ) { $pattern = 'conversion'; $insights[] = sprintf( /* translators: 1: sessions before, 2: sessions now, 3: rate before, 4: rate now. */ __( 'Organic visits held up (%1$s → %2$s) but the conversion rate fell (%3$s → %4$s): people still arrive but fewer complete the form or call. That points to the site (a form, a button or the tracking) rather than to SEO.', 'turtlereport' ), Wording::num( $s_prev ), Wording::num( $s_now ), Wording::pct( $cr_prev ), Wording::pct( $cr_now ) ); } else { $insights[] = sprintf( /* translators: 1: sessions change, 2: rate before, 3: rate now. */ __( 'Both fell: organic visits changed %1$s and the conversion rate went from %2$s to %3$s, so check traffic and the forms.', 'turtlereport' ), Wording::signed( (float) $sc ), Wording::pct( $cr_prev ), Wording::pct( $cr_now ) ); } } $event = $events ? $events[0] : null; if ( $event && ( $event['prev'] - $event['now'] ) / $lost >= 0.6 ) { $insights[] = sprintf( /* translators: 1: event name, 2: before, 3: now. */ __( 'Most of the drop is in “%1$s” (%2$s → %3$s), so start there.', 'turtlereport' ), $event['name'], Wording::num( $event['prev'] ), Wording::num( $event['now'] ) ); } $top_page = $pages ? $pages[0] : null; if ( $top_page && ( $top_page['prev'] - $top_page['now'] ) / $lost >= 0.5 ) { $insights[] = sprintf( /* translators: 1: page, 2: before, 3: now. */ __( '%1$s lost the most (%2$s → %3$s).', 'turtlereport' ), Wording::page_name( $top_page['path'] ), Wording::num( $top_page['prev'] ), Wording::num( $top_page['now'] ) ); } if ( $event ) { $metrics[] = array( 'label' => $event['name'], 'value' => Wording::num( $event['now'] ), 'change' => Wording::change_text( $event['now'], $event['prev'] ), 'tone' => $event['now'] < $event['prev'] ? 'bad' : 'flat', ); } $on = $top_page ? Wording::page_ref( $top_page['path'] ) : __( 'the main landing pages', 'turtlereport' ); if ( 'traffic' === $pattern ) { /* translators: %s: page name. */ $next = sprintf( __( 'Search traffic fell too: start with the pages that lost Google clicks, then test the form on %s.', 'turtlereport' ), $on ); $checks = array( __( 'Start with search traffic: review the pages that lost Google clicks in this client\'s report.', 'turtlereport' ), /* translators: %s: page name. */ sprintf( __( 'Check that %s, which lost the most leads, still ranks, loads and shows its form or call button.', 'turtlereport' ), $on ), __( 'Once traffic is explained, test the forms once yourself.', 'turtlereport' ), ); } else { $next = $event /* translators: 1: event name, 2: page name. */ ? sprintf( __( 'Test “%1$s” on %2$s yourself and check that GA4 records it.', 'turtlereport' ), $event['name'], $on ) /* translators: %s: page name. */ : sprintf( __( 'Test the forms on %s yourself and check that GA4 records them.', 'turtlereport' ), $on ); $checks = array( $event /* translators: 1: event name, 2: page name. */ ? sprintf( __( 'Test the form or button that sends “%1$s” on %2$s yourself, then check that GA4 records it in its real-time report.', 'turtlereport' ), $event['name'], $on ) /* translators: %s: page name. */ : sprintf( __( 'Test the forms on %s yourself, then check that GA4 records them in its real-time report.', 'turtlereport' ), $on ), __( 'Check what changed recently: the form plugin, theme, Google Tag Manager, cookie banner or thank-you page. GA4 event names are case-sensitive.', 'turtlereport' ), __( 'Compare leads by landing page in the report to see whether one page explains the drop.', 'turtlereport' ), ); } $ev_rows = array(); foreach ( $events as $row ) { $ev_rows[] = array( (string) $row['name'], Wording::num( $row['prev'] ), Wording::num( $row['now'] ), Wording::change_text( $row['now'], $row['prev'] ) ); } $pg_rows = array(); foreach ( $pages as $row ) { $pg_rows[] = array( (string) $row['path'], Wording::num( $row['prev'] ), Wording::num( $row['now'] ), Wording::change_text( $row['now'], $row['prev'] ) ); } $tables = array(); if ( $ev_rows ) { $tables[] = array( 'title' => __( 'By event', 'turtlereport' ), 'cols' => array( __( 'Event', 'turtlereport' ), __( 'Before', 'turtlereport' ), __( 'Now', 'turtlereport' ), __( 'Change', 'turtlereport' ) ), 'rows' => $ev_rows, ); } if ( $pg_rows ) { $tables[] = array( 'title' => __( 'Landing pages that lost the most', 'turtlereport' ), 'cols' => array( __( 'Landing page', 'turtlereport' ), __( 'Before', 'turtlereport' ), __( 'Now', 'turtlereport' ), __( 'Change', 'turtlereport' ) ), 'rows' => $pg_rows, ); } $level = Signals::confidence( $prev, array( 40, 15 ) ); $conf = array( 'level' => $level, 'text' => 'early' === $level /* translators: %s: number of leads. */ ? sprintf( __( 'Based on only %s leads in the 28 days before. Small numbers swing a lot, so treat this as an early signal.', 'turtlereport' ), Wording::num( $prev ) ) /* translators: %s: number of leads. */ : sprintf( __( 'Based on %s leads from organic search in the 28 days before.', 'turtlereport' ), Wording::num( $prev ) ), ); return array( 'kind' => $picked ? __( 'Fewer priority conversions', 'turtlereport' ) : __( 'Fewer leads', 'turtlereport' ), /* translators: 1: conversion label, 2: percentage. */ 'headline' => sprintf( __( '%1$s down %2$s', 'turtlereport' ), $label, Wording::pct_abs( Wording::val( $d, array( 'change' ) ) ) ), 'url' => Wording::site_url( $site ), 'path' => __( 'Organic search, all pages', 'turtlereport' ), /* translators: 1: conversion label, 2: percentage. */ 'title' => sprintf( __( '%1$s down %2$s', 'turtlereport' ), $label, Wording::pct_abs( Wording::val( $d, array( 'change' ) ) ) ), /* translators: 1: before, 2: now. */ 'summary' => sprintf( __( '%1$s → %2$s from organic search, last 28 days vs the 28 days before.', 'turtlereport' ), Wording::num( $prev ), Wording::num( $now ) ), 'metrics' => array_slice( $metrics, 0, 4 ), 'insights' => Wording::with_confidence( $insights, $conf ), 'next' => $next, 'checks' => $checks, 'confidence' => $conf, 'facts' => $facts, 'tables' => $tables, ); } /** * A conversion event that stopped recording. * * @param array $d Data. * @return array */ public static function tracking( array $d ) { $event = (string) Wording::val( $d, array( 'event' ), '' ); $before = (int) Wording::val( $d, array( 'before' ) ); $sessions = isset( $d['sessions'] ) ? (int) $d['sessions'] : null; $range = Wording::range( (string) Wording::val( $d, array( 'since' ), '' ), (string) Wording::val( $d, array( 'until' ), '' ) ); $insights = array( sprintf( /* translators: 1: event name, 2: date range, 3: count. */ __( 'GA4 recorded no “%1$s” events from organic search in the last 7 days (%2$s), after %3$s in the 28 days before.', 'turtlereport' ), $event, $range, Wording::num( $before ) ), ); if ( null !== $sessions ) { /* translators: %s: number of sessions. */ $insights[] = sprintf( __( 'The site still had %s organic visits in those 7 days, so people were arriving. A sudden stop like this, while visits continue, usually means the event isn\'t being sent any more (a broken form or tag), not that people stopped converting.', 'turtlereport' ), Wording::num( $sessions ) ); } $metrics = array( array( 'label' => __( 'Last 7 days', 'turtlereport' ), 'value' => '0', 'change' => __( 'events recorded', 'turtlereport' ), 'tone' => 'bad', ), array( 'label' => __( '28 days before', 'turtlereport' ), 'value' => Wording::num( $before ), 'change' => __( 'events recorded', 'turtlereport' ), 'tone' => 'flat', ), ); if ( null !== $sessions ) { $metrics[] = array( 'label' => __( 'Organic visits, last 7 days', 'turtlereport' ), 'value' => Wording::num( $sessions ), 'change' => __( 'people still arriving', 'turtlereport' ), 'tone' => 'flat', ); } return array( 'kind' => __( 'Lead tracking stopped', 'turtlereport' ), /* translators: %s: event name. */ 'headline' => sprintf( __( '“%s” stopped recording', 'turtlereport' ), $event ), 'path' => __( 'GA4 event', 'turtlereport' ), /* translators: %s: event name. */ 'title' => sprintf( __( '“%s” stopped recording', 'turtlereport' ), $event ), /* translators: %s: count. */ 'summary' => sprintf( __( '0 in the last 7 days, after %s in the 28 days before.', 'turtlereport' ), Wording::num( $before ) ), 'metrics' => $metrics, 'insights' => $insights, /* translators: %s: event name. */ 'next' => sprintf( __( 'Test “%s” yourself and check that GA4 records it.', 'turtlereport' ), $event ), 'checks' => array( /* translators: %s: event name. */ sprintf( __( 'Test the form or button that sends “%s” yourself, then check that GA4 records it in its real-time report.', 'turtlereport' ), $event ), __( 'Check what changed on the site in the last week: the form plugin, theme, Google Tag Manager, cookie banner or thank-you page.', 'turtlereport' ), __( 'Check that the event name in GA4 still matches exactly (names are case-sensitive). If it was renamed on purpose, update this site\'s conversion events.', 'turtlereport' ), ), 'confidence' => array( 'level' => $before >= 20 ? 'strong' : 'moderate', /* translators: %s: count. */ 'text' => sprintf( __( 'Based on %s events in the 28 days before, against none in the last 7 days.', 'turtlereport' ), Wording::num( $before ) ), ), ); } /** * Organic sessions down (sites without Search Console). * * @param array $d Data. * @param Profile $site Site. * @return array */ public static function sessions( array $d, Profile $site ) { $now = Wording::val( $d, array( 'now' ) ); $prev = Wording::val( $d, array( 'prev' ) ); return array( 'kind' => __( 'Fewer visits from search', 'turtlereport' ), /* translators: %s: percentage. */ 'headline' => sprintf( __( 'Organic visits down %s', 'turtlereport' ), Wording::pct_abs( Wording::val( $d, array( 'change' ) ) ) ), 'url' => Wording::site_url( $site ), 'path' => __( 'All pages', 'turtlereport' ), /* translators: %s: percentage. */ 'title' => sprintf( __( 'Organic visits down %s', 'turtlereport' ), Wording::pct_abs( Wording::val( $d, array( 'change' ) ) ) ), /* translators: 1: before, 2: now. */ 'summary' => sprintf( __( '%1$s → %2$s GA4 organic sessions, last 28 days vs the 28 days before.', 'turtlereport' ), Wording::num( $prev ), Wording::num( $now ) ), 'metrics' => array( Wording::metric( __( 'Organic sessions', 'turtlereport' ), Wording::num( $now ), Wording::val( $d, array( 'change' ) ) ) ), 'insights' => array( /* translators: 1: before, 2: now. */ sprintf( __( 'GA4 recorded fewer organic visits (%1$s → %2$s).', 'turtlereport' ), Wording::num( $prev ), Wording::num( $now ) ), __( 'Without Search Console on this site, TurtleReport can\'t see whether rankings, impressions or clicks changed, so the cause can\'t be narrowed down from here.', 'turtlereport' ), ), 'next' => __( 'Check that the site loads and GA4 still records visits.', 'turtlereport' ), 'checks' => array( __( 'Connect Search Console for this site to see which pages and search queries lost visits.', 'turtlereport' ), __( 'Open the website and check it loads, and that GA4 still records visits in its real-time report.', 'turtlereport' ), __( 'Compare the same weeks last year in GA4 to rule out a seasonal dip.', 'turtlereport' ), ), 'facts' => array( Wording::fact( __( 'Organic sessions', 'turtlereport' ), $prev, $now ) ), ); } }