From 770221425b7b3a03685182c1a65ef13cff301ad8 Mon Sep 17 00:00:00 2001 From: Jefferson Rabb Date: Mon, 7 Dec 2020 11:51:36 -0500 Subject: [PATCH 1/3] Add/persist visit time (#335) * feat: change date column to datetime, record timestamp * test: fix tests --- api/segmentation/class-segmentation-report.php | 2 +- includes/class-newspack-popups-segmentation.php | 4 +++- tests/test-segmentation.php | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/segmentation/class-segmentation-report.php b/api/segmentation/class-segmentation-report.php index e27da400..08ce216c 100644 --- a/api/segmentation/class-segmentation-report.php +++ b/api/segmentation/class-segmentation-report.php @@ -22,7 +22,7 @@ public static function log_single_visit( $payload ) { [ 'post_read', $payload['clientId'], - isset( $payload['date'] ) ? $payload['date'] : gmdate( 'Y-m-d', time() ), + isset( $payload['date'] ) ? $payload['date'] : gmdate( 'Y-m-d H:i:s', time() ), $payload['post_id'], $payload['categories'], ] diff --git a/includes/class-newspack-popups-segmentation.php b/includes/class-newspack-popups-segmentation.php index e1e54d3a..4c431c40 100644 --- a/includes/class-newspack-popups-segmentation.php +++ b/includes/class-newspack-popups-segmentation.php @@ -258,7 +258,7 @@ public static function create_database_table() { $sql = "CREATE TABLE $events_table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, - created_at date NOT NULL, + created_at datetime NOT NULL, -- type of event type varchar(20) NOT NULL, -- Unique id of a device/browser pair @@ -274,6 +274,8 @@ public static function create_database_table() { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $sql ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.dbDelta_dbdelta + } elseif ( 'date' === $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM {$events_table_name} LIKE %s", 'created_at' ), 1 ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( "ALTER TABLE {$events_table_name} CHANGE `created_at` `created_at` DATETIME NOT NULL" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared } } diff --git a/tests/test-segmentation.php b/tests/test-segmentation.php index f6e0af93..d602b16e 100644 --- a/tests/test-segmentation.php +++ b/tests/test-segmentation.php @@ -58,7 +58,7 @@ public function test_log_visit() { [ 'post_read', self::$post_read_payload['clientId'], - gmdate( 'Y-m-d', time() ), + gmdate( 'Y-m-d H:i:s', time() ), self::$post_read_payload['post_id'], self::$post_read_payload['categories'], ] From 326fb103d0eb9f05321c5598eac47ced378dcc34 Mon Sep 17 00:00:00 2001 From: Adam Borowski Date: Mon, 7 Dec 2020 14:47:31 +0100 Subject: [PATCH 2/3] feat: move custom GA config endpoint to lightweight API --- api/classes/class-lightweight-api.php | 14 ++- .../class-segmentation-custom-ga-config.php | 99 +++++++++++++++++++ api/segmentation/class-segmentation.php | 7 ++ api/segmentation/index.php | 3 + includes/class-newspack-popups-api.php | 86 ---------------- .../class-newspack-popups-segmentation.php | 38 ++++--- 6 files changed, 143 insertions(+), 104 deletions(-) create mode 100644 api/segmentation/class-segmentation-custom-ga-config.php diff --git a/api/classes/class-lightweight-api.php b/api/classes/class-lightweight-api.php index 5caf0a24..4c6fd269 100644 --- a/api/classes/class-lightweight-api.php +++ b/api/classes/class-lightweight-api.php @@ -137,7 +137,7 @@ public function get_transient( $name ) { return null; } elseif ( false === $value ) { $this->debug['read_query_count'] += 1; - $value = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $name ) ); // phpcs:ignore + $value = $this->get_option( $name ); if ( $value ) { wp_cache_set( $name, $value, 'newspack-popups' ); } else { @@ -153,7 +153,7 @@ public function get_transient( $name ) { /** * Upsert transient. * - * @param string $name THe transient's name. + * @param string $name The transient's name. * @param string $value THe transient's value. */ public function set_transient( $name, $value ) { @@ -309,4 +309,14 @@ public function get_post_payload() { } return $payload; } + + /** + * Get option. + * + * @param string $name Option name. + */ + public function get_option( $name ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + } } diff --git a/api/segmentation/class-segmentation-custom-ga-config.php b/api/segmentation/class-segmentation-custom-ga-config.php new file mode 100644 index 00000000..0ba50476 --- /dev/null +++ b/api/segmentation/class-segmentation-custom-ga-config.php @@ -0,0 +1,99 @@ +response = $this->get_custom_analytics_configuration( $_REQUEST ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $this->respond(); + } + + /** + * Get custom Analytics config, with segmentation-related custom dimensions assigned. + * The pageviews will be reported using this configuration, so it's important + * to include the custom dimensions set up by the Newspack Plugin, too. + * + * @param Request $request Request object. + */ + public function get_custom_analytics_configuration( $request ) { + $client_id = $request['client_id']; + $ga_settings = maybe_unserialize( $this->get_option( 'googlesitekit_analytics_settings' ) ); + if ( ! $client_id || ! $ga_settings || ! isset( $ga_settings['propertyID'] ) ) { + return []; + } + + $custom_dimensions = json_decode( $request['custom_dimensions'] ); + + // Tracking ID from Site Kit. + $gtag_id = $ga_settings['propertyID']; + + $custom_dimensions_values = []; + + $api = new Lightweight_API(); + $client_data = $api->get_client_data( $client_id ); + + foreach ( $custom_dimensions as $custom_dimension ) { + // Strip the `ga:` prefix from gaID. + $dimension_id = substr( $custom_dimension->gaID, 3 ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + switch ( $custom_dimension->role ) { + case Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_READER_FREQUENCY: + $read_count = count( $client_data['posts_read'] ); + // Tiers mimick NCI's – https://news-consumer-insights.appspot.com. + $read_count_tier = 'casual'; + if ( $read_count > 1 && $read_count <= 14 ) { + $read_count_tier = 'loyal'; + } elseif ( $read_count > 14 ) { + $read_count_tier = 'brand_lover'; + } + $custom_dimensions_values[ $dimension_id ] = $read_count_tier; + break; + case Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_IS_SUBSCRIBER: + $custom_dimensions_values[ $dimension_id ] = Campaign_Data_Utils::is_subscriber( $client_data, wp_get_referer() ); + break; + case Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_IS_DONOR: + $custom_dimensions_values[ $dimension_id ] = Campaign_Data_Utils::is_donor( $client_data ); + break; + } + } + + $custom_dimensions_existing_values = (array) json_decode( $request['custom_dimensions_existing_values'] ); + + // This is an AMP Analytics-compliant configuration, which on non-AMP pages will be + // processed by this plugin's amp-analytics polyfill (src/view). + return [ + 'vars' => [ + 'gtag_id' => $gtag_id, + 'config' => [ + $gtag_id => array_merge( + [ + 'groups' => 'default', + ], + $custom_dimensions_values, + $custom_dimensions_existing_values + ), + ], + ], + 'optoutElementId' => '__gaOptOutExtension', + ]; + } +} +new Segmentation_Custom_GA_Config(); diff --git a/api/segmentation/class-segmentation.php b/api/segmentation/class-segmentation.php index 72befe6f..29592c37 100644 --- a/api/segmentation/class-segmentation.php +++ b/api/segmentation/class-segmentation.php @@ -11,6 +11,13 @@ * Manages Segmentation. */ class Segmentation { + /** + * Names of custom dimensions options. + */ + const CUSTOM_DIMENSIONS_OPTION_NAME_READER_FREQUENCY = 'newspack_popups_cd_reader_frequency'; + const CUSTOM_DIMENSIONS_OPTION_NAME_IS_SUBSCRIBER = 'newspack_popups_cd_is_subscriber'; + const CUSTOM_DIMENSIONS_OPTION_NAME_IS_DONOR = 'newspack_popups_cd_is_donor'; + /** * Get log file path. */ diff --git a/api/segmentation/index.php b/api/segmentation/index.php index 28891c4e..7993bfd8 100644 --- a/api/segmentation/index.php +++ b/api/segmentation/index.php @@ -8,6 +8,9 @@ require_once '../setup.php'; switch ( $_SERVER['REQUEST_METHOD'] ) { //phpcs:ignore + case 'GET': + include './class-segmentation-custom-ga-config.php'; + break; case 'POST': include './class-segmentation-client-data.php'; break; diff --git a/includes/class-newspack-popups-api.php b/includes/class-newspack-popups-api.php index b4aa17ec..39af85b8 100644 --- a/includes/class-newspack-popups-api.php +++ b/includes/class-newspack-popups-api.php @@ -69,92 +69,6 @@ public function register_api_endpoints() { ], ] ); - \register_rest_route( - 'newspack-popups/v1', - 'analytics-config', - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'get_custom_analytics_configuration' ], - 'permission_callback' => '__return_true', - ] - ); - } - - /** - * Get custom Analytics config, with segmentation-related custom dimensions assigned. - * The pageviews will be reported using this configuration, so it's important - * to include the custom dimensions set up by the Newspack Plugin, too. - * - * @param WP_REST_Request $request Request object. - */ - public static function get_custom_analytics_configuration( $request ) { - $client_id = $request['client_id']; - $ga_settings = get_option( 'googlesitekit_analytics_settings' ); - if ( ! $client_id || ! $ga_settings || ! isset( $ga_settings['propertyID'] ) ) { - return []; - } - - $custom_dimensions = []; - if ( class_exists( 'Newspack\Analytics_Wizard' ) ) { - $custom_dimensions = Newspack\Analytics_Wizard::list_configured_custom_dimensions(); - } - - // Tracking ID from Site Kit. - $gtag_id = $ga_settings['propertyID']; - - $custom_dimensions_values = []; - - require_once dirname( __FILE__ ) . '/../api/classes/class-lightweight-api.php'; - require_once dirname( __FILE__ ) . '/../api/campaigns/class-campaign-data-utils.php'; - $api = new Lightweight_API(); - $client_data = $api->get_client_data( $client_id ); - - foreach ( $custom_dimensions as $custom_dimension ) { - // Strip the `ga:` prefix from gaID. - $dimension_id = substr( $custom_dimension['gaID'], 3 ); - switch ( $custom_dimension['role'] ) { - case Newspack_Popups_Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_READER_FREQUENCY: - $read_count = count( $client_data['posts_read'] ); - // Tiers mimick NCI's – https://news-consumer-insights.appspot.com. - $read_count_tier = 'casual'; - if ( $read_count > 1 && $read_count <= 14 ) { - $read_count_tier = 'loyal'; - } elseif ( $read_count > 14 ) { - $read_count_tier = 'brand_lover'; - } - $custom_dimensions_values[ $dimension_id ] = $read_count_tier; - break; - case Newspack_Popups_Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_IS_SUBSCRIBER: - $custom_dimensions_values[ $dimension_id ] = Campaign_Data_Utils::is_subscriber( $client_data, wp_get_referer() ); - break; - case Newspack_Popups_Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_IS_DONOR: - $custom_dimensions_values[ $dimension_id ] = Campaign_Data_Utils::is_donor( $client_data ); - break; - } - } - - $custom_dimensions_existing_values = []; - if ( class_exists( 'Newspack\Analytics' ) ) { - $custom_dimensions_existing_values = Newspack\Analytics::get_custom_dimensions_values( $request['post_id'] ); - } - - // This is an AMP Analytics-compliant configuration, which on non-AMP pages will be - // processed by this plugin's amp-analytics polyfill (src/view). - return [ - 'vars' => [ - 'gtag_id' => $gtag_id, - 'config' => [ - $gtag_id => array_merge( - [ - 'groups' => 'default', - ], - $custom_dimensions_values, - $custom_dimensions_existing_values - ), - ], - ], - 'optoutElementId' => '__gaOptOutExtension', - ]; } /** diff --git a/includes/class-newspack-popups-segmentation.php b/includes/class-newspack-popups-segmentation.php index 4c431c40..58617040 100644 --- a/includes/class-newspack-popups-segmentation.php +++ b/includes/class-newspack-popups-segmentation.php @@ -35,13 +35,6 @@ final class Newspack_Popups_Segmentation { */ const SEGMENTS_OPTION_NAME = 'newspack_popups_segments'; - /** - * Names of custom dimensions options. - */ - const CUSTOM_DIMENSIONS_OPTION_NAME_READER_FREQUENCY = 'newspack_popups_cd_reader_frequency'; - const CUSTOM_DIMENSIONS_OPTION_NAME_IS_SUBSCRIBER = 'newspack_popups_cd_is_subscriber'; - const CUSTOM_DIMENSIONS_OPTION_NAME_IS_DONOR = 'newspack_popups_cd_is_donor'; - /** * Main Newspack Segmentation Plugin Instance. * Ensures only one instance of Newspack Segmentation Plugin Instance is loaded or can be loaded. @@ -109,23 +102,23 @@ public static function register_custom_dimensions( $default_dimensions ) { $default_dimensions, [ [ - 'role' => self::CUSTOM_DIMENSIONS_OPTION_NAME_READER_FREQUENCY, + 'role' => Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_READER_FREQUENCY, 'option' => [ - 'value' => self::CUSTOM_DIMENSIONS_OPTION_NAME_READER_FREQUENCY, + 'value' => Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_READER_FREQUENCY, 'label' => __( 'Reader frequency', 'newspack' ), ], ], [ - 'role' => self::CUSTOM_DIMENSIONS_OPTION_NAME_IS_SUBSCRIBER, + 'role' => Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_IS_SUBSCRIBER, 'option' => [ - 'value' => self::CUSTOM_DIMENSIONS_OPTION_NAME_IS_SUBSCRIBER, + 'value' => Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_IS_SUBSCRIBER, 'label' => __( 'Is a subcriber', 'newspack' ), ], ], [ - 'role' => self::CUSTOM_DIMENSIONS_OPTION_NAME_IS_DONOR, + 'role' => Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_IS_DONOR, 'option' => [ - 'value' => self::CUSTOM_DIMENSIONS_OPTION_NAME_IS_DONOR, + 'value' => Segmentation::CUSTOM_DIMENSIONS_OPTION_NAME_IS_DONOR, 'label' => __( 'Is a donor', 'newspack' ), ], ], @@ -134,6 +127,7 @@ public static function register_custom_dimensions( $default_dimensions ) { return $default_dimensions; } + /** * Get GA property ID from Site Kit's options. */ @@ -148,12 +142,24 @@ public static function get_ga_property_id() { * Inset GTAG amp-analytics with a remote config, which will insert segmentation-related custom dimensions. */ public static function insert_gtag_amp_analytics() { + + $custom_dimensions = []; + if ( class_exists( 'Newspack\Analytics_Wizard' ) ) { + $custom_dimensions = Newspack\Analytics_Wizard::list_configured_custom_dimensions(); + } + $custom_dimensions_existing_values = []; + if ( class_exists( 'Newspack\Analytics' ) ) { + $custom_dimensions_existing_values = Newspack\Analytics::get_custom_dimensions_values( get_the_ID() ); + } + $remote_config_url = add_query_arg( [ - 'client_id' => 'CLIENT_ID(' . esc_attr( self::NEWSPACK_SEGMENTATION_CID_NAME ) . ')', - 'post_id' => esc_attr( get_the_ID() ), + 'client_id' => 'CLIENT_ID(' . esc_attr( self::NEWSPACK_SEGMENTATION_CID_NAME ) . ')', + 'post_id' => esc_attr( get_the_ID() ), + 'custom_dimensions' => wp_json_encode( $custom_dimensions ), + 'custom_dimensions_existing_values' => wp_json_encode( $custom_dimensions_existing_values ), ], - get_rest_url( null, 'newspack-popups/v1/analytics-config' ) + plugins_url( '../api/segmentation/index.php', __FILE__ ) ); ?> From 14120a8d677dea5912cc0a6224d0de5f623aae89 Mon Sep 17 00:00:00 2001 From: Adam Borowski Date: Tue, 8 Dec 2020 13:47:37 +0100 Subject: [PATCH 3/3] fix: do create config file on the atomic platform --- includes/class-newspack-popups.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-newspack-popups.php b/includes/class-newspack-popups.php index 2e7a3efd..3f764d24 100644 --- a/includes/class-newspack-popups.php +++ b/includes/class-newspack-popups.php @@ -454,9 +454,9 @@ public static function popup_default_fields( $post_id, $post, $update ) { * Create the config file for the API, unless it exists. */ public static function create_lightweight_api_config() { - // Don't create a config file on Newspack's Atomic platform, or if there is a file already. + // Don't create a config file if not on Newspack's Atomic platform, or if there is a file already. if ( - defined( 'ATOMIC_SITE_ID' ) || + ! ( defined( 'ATOMIC_SITE_ID' ) && ATOMIC_SITE_ID ) || ( file_exists( self::LIGHTWEIGHT_API_CONFIG_FILE_PATH_LEGACY ) || file_exists( self::LIGHTWEIGHT_API_CONFIG_FILE_PATH ) ) ) { return;