Skip to content

Commit

Permalink
Merge pull request google#8406 from google/issue/8269-decoupled-gtag-…
Browse files Browse the repository at this point in the history
…infra

Issue / 8269 Decoupled GTag Infra
  • Loading branch information
tofumatt authored Mar 20, 2024
2 parents 334bf0d + ca5c1f9 commit ba6c636
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 0 deletions.
191 changes: 191 additions & 0 deletions includes/Core/Tags/GTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php
/**
* Class Google\Site_Kit\Core\Tags\GTag
*
* @package Google\Site_Kit
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/

namespace Google\Site_Kit\Core\Tags;

use Google\Site_Kit\Core\Util\Method_Proxy_Trait;

/**
* Class to handle gtag rendering across modules.
*
* @since n.e.x.t
* @access public
* @ignore
*/
class GTag {
use Method_Proxy_Trait;

const HANDLE = 'google_gtagjs';
/**
* Holds an array of gtag ID's and their inline config elements.
*
* @var array $tags Array of tag ID's and their configs.
*/
private $tags = array();
/**
* Holds an array of gtag commands, their parameters and command positions.
*
* @var array $commands Array of gtag config commands.
*/
private $commands = array();

/**
* Register method called after class instantiation.
*
* @since n.e.x.t
* @access public
*
* @return void
*/
public function register() {
add_action( 'wp_enqueue_scripts', $this->get_method_proxy( 'enqueue_gtag_script' ), 20 );

add_filter(
'wp_resource_hints',
function ( $urls, $relation_type ) {
if ( 'dns-prefetch' === $relation_type ) {
$urls[] = '//www.googletagmanager.com';
}

return $urls;
},
10,
2
);
}

/**
* Method to add a gtag ID and config for output rendering.
*
* @since n.e.x.t
* @access public
*
* @param string $tag_id The gtag ID.
* @param array $config Array of inline gtag config values.
*
* @return void
*/
public function add_tag( $tag_id, $config = array() ) {
$this->tags[] = array(
'tag_id' => $tag_id,
'config' => $config,
);
}

/**
* Method to add a gtag command, associated parameters and output position.
*
* @since n.e.x.t
* @access public
*
* @param string $command The gtag command to add.
* @param array $parameters Array of command parameters.
* @param string $position Position of command. "before|after".
*
* @return void
*/
public function add_command( $command, $parameters, $position = 'after' ) {
$this->commands[] = array(
'command' => $command, // e.g. 'config', 'event', etc.
'parameters' => $parameters, // e.g. array( 'send_to', 'AW-123456789' ).
'position' => $position, // e.g. 'after', 'before'. This determines the position of the inline script relative to the gtag.js script.
);
}

/**
* Method used to enqueue the gtag script along with additional tags,
* configs and commands.
*
* @since n.e.x.t
* @access protected
*
* @return void
*/
protected function enqueue_gtag_script() {
// $this->tags and $this->commands will be populated via this action's handlers.
do_action( 'googlesitekit_setup_gtag', $this );

if ( empty( $this->tags ) ) {
return;
}

// Load the GTag scripts using the first tag ID - it doesn't matter which is used, all registered tags will be set up with a
// config command regardless of which is used to load the source.
$gtag_src = 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode( $this->tags[0]['tag_id'] );

// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_enqueue_script( self::HANDLE, $gtag_src, false, null, false );
wp_script_add_data( self::HANDLE, 'script_execution', 'async' );

// Note that `gtag()` may already be defined via the `Consent_Mode` output, but this is safe to call multiple times.
wp_add_inline_script( self::HANDLE, 'window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}' );
wp_add_inline_script( self::HANDLE, 'gtag("js", new Date());' );
wp_add_inline_script( self::HANDLE, 'gtag("set", "developer_id.dZTNiMT", true);' ); // Site Kit developer ID.

foreach ( $this->tags as $tag ) {
wp_add_inline_script( self::HANDLE, $this->get_gtag_call_for_tag( $tag ) );
}

foreach ( $this->commands as $command ) {
wp_add_inline_script( self::HANDLE, $this->get_gtag_call_for_command( $command ), $command['position'] );
}

$filter_google_gtagjs = function ( $tag, $handle ) {
if ( self::HANDLE !== $handle ) {
return $tag;
}

$snippet_comment_begin = sprintf( "\n<!-- %s -->\n", esc_html__( 'Google tag (gtag.js) snippet added by Site Kit', 'google-site-kit' ) );
$snippet_comment_end = sprintf( "\n<!-- %s -->\n", esc_html__( 'End Google tag (gtag.js) snippet added by Site Kit', 'google-site-kit' ) );

return $snippet_comment_begin . $tag . $snippet_comment_end;
};

add_filter( 'script_loader_tag', $filter_google_gtagjs, 20, 2 );
}

/**
* Method used to return gtag() config call for selected tag.
*
* @since n.e.x.t
* @access protected
*
* @param array $tag The Gtag tag, along with its config parameters.
*
* @return string Gtag call for tag in question.
*/
protected function get_gtag_call_for_tag( $tag ) {
return empty( $tag['config'] )
? sprintf( 'gtag("config", "%s");', esc_js( $tag['tag_id'] ) )
: sprintf( 'gtag("config", "%s", %s);', esc_js( $tag['tag_id'] ), wp_json_encode( $tag['config'] ) );
}

/**
* Method used to return gtag call for specific command.
*
* @since n.e.x.t
* @access protected
*
* @param array $command The command array with applicable command and params.
*
* @return string Gtag function call for specific command.
*/
protected function get_gtag_call_for_command( $command ) {
$gtag_args = array_merge( array( $command['command'] ), $command['parameters'] );
$gtag_args = array_map(
function( $arg ) {
return wp_json_encode( $arg );
},
$gtag_args
);

return sprintf( 'gtag(%s);', implode( ',', $gtag_args ) );
}
}
1 change: 1 addition & 0 deletions includes/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ function() use ( $options, $activation_flag ) {
( new Core\Key_Metrics\Key_Metrics( $this->context, $user_options, $options ) )->register();
( new Core\Prompts\Prompts( $this->context, $user_options ) )->register();
( new Core\Consent_Mode\Consent_Mode( $this->context, $options ) )->register();
( new Core\Tags\GTag() )->register();

// If a login is happening (runs after 'init'), update current user in dependency chain.
add_action(
Expand Down
106 changes: 106 additions & 0 deletions tests/phpunit/integration/Core/Tags/GTagTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php
/**
* Class Google\Site_Kit\Tests\Tags\GTagTest
*
* @package Google\Site_Kit\Tests\Core\Tags\GTag
* @copyright 2024 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*/

namespace Google\Site_Kit\Tests\Core\Tags;

use Google\Site_Kit\Core\Tags\GTag;
use Google\Site_Kit\Tests\TestCase;

class GTagTest extends TestCase {

/**
* Holds an instance of the GTag class.
*
* @var GTag $gtag Gtag instance.
*/
private $gtag;

const TEST_TAG_ID_1 = 'GT-12345';
const TEST_TAG_ID_2 = 'GT-67890';
const TEST_TAG_ID_2_CONFIG = array( 'foo' => 'bar' );
const TEST_COMMAND_1 = 'foo';
const TEST_COMMAND_1_POSITION = 'before';
const TEST_COMMAND_1_PARAMS = array( 'bar', 'far' );
const TEST_COMMAND_2_POSITION = 'after';
const TEST_COMMAND_2 = 'foo';
const TEST_COMMAND_2_PARAMS = array( array( 'bar' => 'far' ) );

public function set_up() {
parent::set_up();

$this->gtag = new GTag();
$this->gtag->register();

$this->gtag->add_tag( static::TEST_TAG_ID_1 );

// Add commands for testing.
$this->gtag->add_command( static::TEST_COMMAND_1, static::TEST_COMMAND_1_PARAMS, static::TEST_COMMAND_1_POSITION );
$this->gtag->add_command( static::TEST_COMMAND_2, static::TEST_COMMAND_2_PARAMS, static::TEST_COMMAND_2_POSITION );
}

public function test_gtag_class_instance() {
$this->assertInstanceOf( GTag::class, $this->gtag );
}

public function test_gtag_script_enqueue() {
$this->assertFalse( wp_script_is( GTag::HANDLE ) );

do_action( 'wp_enqueue_scripts' );

// Assert that the gtag script is enqueued.
$this->assertTrue( wp_script_is( GTag::HANDLE ) );
}

public function test_gtag_script_src() {
$scripts = wp_scripts();
$script = $scripts->registered[ GTag::HANDLE ];

// Assert that the gtag script src is correct.
$this->assertEquals( 'https://www.googletagmanager.com/gtag/js?id=' . static::TEST_TAG_ID_1, $script->src );
}

public function test_gtag_script_contains_gtag_call() {
$scripts = wp_scripts();
$script = $scripts->registered[ GTag::HANDLE ];

// Assert the array of inline script data contains the necessary gtag config line.
// Should be in index 4, the first registered gtag.
$this->assertEquals( 'gtag("config", "' . static::TEST_TAG_ID_1 . '");', $script->extra['after'][4] );
}

public function test_gtag_script_commands() {
$scripts = wp_scripts();
$script = $scripts->registered[ GTag::HANDLE ];

// Test commands in the before position.
$this->assertEquals( sprintf( 'gtag(%s");', '"' . static::TEST_COMMAND_1 . '","' . implode( '","', static::TEST_COMMAND_1_PARAMS ) ), $script->extra['before'][1] );

// Test commands in the after position.
$this->assertEquals( sprintf( 'gtag(%s);', '"' . static::TEST_COMMAND_2 . '",' . json_encode( static::TEST_COMMAND_2_PARAMS[0] ) ), $script->extra['after'][5] );
}

public function test_gtag_with_tag_config() {
$this->gtag->add_tag( static::TEST_TAG_ID_2, static::TEST_TAG_ID_2_CONFIG );

// Remove already enqueued script to avoid duplication of output.
global $wp_scripts;
unset( $wp_scripts->registered[ GTag::HANDLE ] );

do_action( 'wp_enqueue_scripts' );

$scripts = wp_scripts();
$script = $scripts->registered[ GTag::HANDLE ];

// Assert the array of inline script data contains the necessary gtag entry for the second script.
// Should be in index 5, immediately after the first registered gtag.
$this->assertEquals( 'gtag("config", "' . static::TEST_TAG_ID_2 . '", ' . json_encode( self::TEST_TAG_ID_2_CONFIG ) . ');', $script->extra['after'][5] );
}

}

0 comments on commit ba6c636

Please sign in to comment.