Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
experience and the iframe embed do. Turn it off and the experience reports
nothing to Ceros; the experience's analytics events still reach the page, so
your own analytics keeps working. The block's editor preview, WordPress's post
preview and the Customizer never report, whatever the switch says. Inline and
server-rendered reporting needs an experience published after Ceros added it
to the manifest, and the account's Ceros Analytics setting on.
preview and the Customizer never report, whatever the switch says. Neither
does a server-rendered experience stored on your site (Store mode): the stored
copy is independent of Ceros, so the switch is off and can't be turned on until
you remove the stored copy. Inline and server-rendered reporting needs an
experience published after Ceros added it to the manifest, and the account's
Ceros Analytics setting on.

### Fixed
- **The plugin reports an old plugin, not a bad API key.** The Ceros API rejects
Expand Down
24 changes: 23 additions & 1 deletion includes/flex-analytics.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
* Previews never track: the editor's, WordPress's post preview and the
* Customizer's. An author checking their own page is not an audience.
*
* Store mode never tracks either. A stored copy doesn't include the reporting
* bundle, which the shell would still fetch from Ceros, and Ceros analytics
* events aren't versioned, so a stored runtime that falls behind could report
* events the pipeline no longer reads. Store mode is for a page that doesn't
* depend on Ceros, so it reports nothing.
*
* @package ceros
*/

Expand All @@ -33,7 +39,7 @@
* @return bool True to report.
*/
function ceros_flex_tracking_enabled( $attributes, $is_preview ) {
if ( $is_preview ) {
if ( $is_preview || ceros_flex_is_store_mode( $attributes ) ) {
return false;
}

Expand All @@ -43,6 +49,22 @@ function ceros_flex_tracking_enabled( $attributes, $is_preview ) {
return false !== ( $attributes['cerosAnalytics'] ?? true );
}

/**
* Whether a block renders in SSR Store mode: SSR delivery from a copy stored on
* this site. A stored copy left behind after switching to another delivery mode
* doesn't count, because render.php only reads it for SSR. Mirrors
* isStoreMode() in src/ceros/constants.js.
*
* Applies whatever the block falls back to when the stored copy can't be read,
* so a Store-mode block never reports.
*
* @param array $attributes Block attributes.
* @return bool True in Store mode.
*/
function ceros_flex_is_store_mode( $attributes ) {
return 'ssr' === ( $attributes['deliveryMode'] ?? '' ) && ! empty( $attributes['storedIndexPath'] );
}

/**
* Whether a block embeds a Flex experience. Flex blocks carry a manifest URL,
* except those saved before it was stored, which carry only the Inline snippet.
Expand Down
25 changes: 20 additions & 5 deletions src/ceros/components/sidebar-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ACTION_TYPES,
EMBED_OPTIONS,
DELIVERY_MODES,
isStoreMode,
manifestUrlFromInline,
} from '../constants';
import { StoreControls } from './store-controls';
Expand Down Expand Up @@ -58,6 +59,8 @@ export function SidebarControls( {
// includes/flex-analytics.php, which applies the choice at render. Legacy
// Studio embeds have no manifest and no way to switch Ceros analytics off.
const isFlex = hasInline || Boolean( attributes?.manifestUrl );
// SSR Store mode never reports to Ceros (see isStoreMode()).
const isStored = isStoreMode( attributes );
return (
<InspectorControls>
<PanelBody
Expand Down Expand Up @@ -290,11 +293,23 @@ export function SidebarControls( {
<ToggleControl
__nextHasNoMarginBottom
label={ __( 'Ceros analytics tracking', 'ceros' ) }
help={ __(
'Reports views and interactions to Ceros Analytics, as the published experience does. Turn it off to report nothing to Ceros; your own analytics still receives the experience鈥檚 events. Previews are never tracked.',
'ceros'
) }
checked={ attributes?.cerosAnalytics !== false }
help={
isStored
? __(
'Stored copies don鈥檛 report to Ceros Analytics. Remove the stored copy to use Ceros Analytics again.',
'ceros'
)
: __(
'Reports views and interactions to Ceros Analytics, as the published experience does. Turn it off to report nothing to Ceros; your own analytics still receives the experience鈥檚 events. Previews are never tracked.',
'ceros'
)
}
// Store mode is shown as off without changing the saved
// choice, which comes back if the stored copy is removed.
checked={
! isStored && attributes?.cerosAnalytics !== false
}
disabled={ isStored }
onChange={ ( value ) =>
setAttributes( { cerosAnalytics: value } )
}
Expand Down
17 changes: 17 additions & 0 deletions src/ceros/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ export const DELIVERY_MODES = {
SSR: 'ssr',
};

/**
* Whether a block renders in SSR Store mode: SSR delivery from a copy stored on
* the site. A stored copy left behind after switching to another delivery mode
* doesn't count, because the server only reads it for SSR. Mirrors
* `ceros_flex_is_store_mode()` in includes/flex-analytics.php, which keeps
* Ceros analytics off in Store mode.
*
* @param {Object} attributes Block attributes.
* @return {boolean} True in Store mode.
*/
export function isStoreMode( attributes ) {
return (
attributes?.deliveryMode === DELIVERY_MODES.SSR &&
Boolean( attributes?.storedIndexPath )
);
}

/**
* Undo the entity encoding wp_kses applies to attribute values, which rewrites a
* bare `&` as `&#038;`. Parsing in a detached textarea keeps the content as raw
Expand Down
20 changes: 20 additions & 0 deletions tests/e2e/tests/analytics-tracking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ test.describe('Ceros analytics tracking', { tag: [TAGS.cerosBlock, TAGS.rendered
await expect(published.iframeEmbed).toHaveAttribute(ANALYTICS, 'disabled')
})
})

test.describe('in SSR Store mode, with the switch on', () => {
test.use({
postOptions: {
title: 'flex analytics store mode',
blocks: [
unfetchableFlexBlock(cerosFlexExperienceUrl(), { cerosAnalytics: true, stored: true }),
],
status: 'publish',
},
})

// A stored copy has no reporting bundle, so Store mode never reports, even
// when the stored copy can't be read and the block falls back.
test('does not report', async ({ page, post }) => {
const published = new PublishedPostPage(page)
await published.open(post.id)
await expect(published.iframeEmbed).toHaveAttribute(ANALYTICS, 'disabled')
})
})
})

test.describe('in a preview, with the switch at its default', () => {
Expand Down
10 changes: 8 additions & 2 deletions tests/e2e/utils/block-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type CerosBlockAttributes = {
fullHeightEmbedCode: string
scrollableEmbedCode: string
storedAt: string
storedIndexPath: string
storedPublishedAt: string
inlineEmbedCode: string
cerosAnalytics: boolean
Expand Down Expand Up @@ -54,15 +55,20 @@ export const resolvedFlexBlock = (
* stored iframe embed code and stamps the block's tracking choice onto it. The
* manifest URL is http, which the fetch rejects before making a request, so the
* render needs no network. `withManifestUrl: false` models a block saved before
* the manifest URL was stored, which carries only the Inline snippet.
* the manifest URL was stored, which carries only the Inline snippet. `stored`
* puts the block in SSR Store mode with a stored copy that isn't on disk, so the
* render still falls through to the stored embed code.
*/
export const unfetchableFlexBlock = (
experienceUrl: string,
options: { cerosAnalytics?: boolean; withManifestUrl?: boolean } = {},
options: { cerosAnalytics?: boolean; withManifestUrl?: boolean; stored?: boolean } = {},
): string => {
const manifestUrl = `${experienceUrl.replace(/^https:/, 'http:')}/manifest.v1.json`
return cerosBlock({
...(options.withManifestUrl === false ? {} : { manifestUrl }),
...(options.stored
? { deliveryMode: 'ssr', storedIndexPath: 'ceros-flex/0/not-stored/v1/index.json' }
: {}),
inlineEmbedCode: `<div data-flex-inline data-flex-manifest-url="${manifestUrl}"></div>`,
fullHeightEmbedCode: `<div data-embed-width="100%" data-embed-height="auto" data-ceros-experience="${experienceUrl}"></div>`,
...(options.cerosAnalytics === undefined ? {} : { cerosAnalytics: options.cerosAnalytics }),
Expand Down
48 changes: 48 additions & 0 deletions tests/js/constants.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { describe, it, expect } from 'vitest';
import {
manifestUrlFromInline,
isStoreMode,
EMBED_OPTIONS,
DELIVERY_MODES,
} from '../../src/ceros/constants';
Expand Down Expand Up @@ -107,3 +108,50 @@ describe( 'shared constant values', () => {
} );
} );
} );

/**
* Store mode keeps Ceros analytics off, so the sidebar and render.php
* (`ceros_flex_is_store_mode()`) must agree on what counts as Store mode.
*/
describe( 'isStoreMode', () => {
const storedIndexPath = 'ceros-flex/12/exp/v1/index.json';

it( 'is SSR rendering from a stored copy', () => {
expect(
isStoreMode( { deliveryMode: DELIVERY_MODES.SSR, storedIndexPath } )
).toBe( true );
} );

it( 'is not live SSR', () => {
expect( isStoreMode( { deliveryMode: DELIVERY_MODES.SSR } ) ).toBe(
false
);
expect(
isStoreMode( {
deliveryMode: DELIVERY_MODES.SSR,
storedIndexPath: '',
} )
).toBe( false );
} );

it( 'ignores a stored copy left behind by another delivery mode', () => {
expect(
isStoreMode( {
deliveryMode: DELIVERY_MODES.INLINE,
storedIndexPath,
} )
).toBe( false );
expect(
isStoreMode( {
deliveryMode: DELIVERY_MODES.IFRAME,
storedIndexPath,
} )
).toBe( false );
expect( isStoreMode( { storedIndexPath } ) ).toBe( false );
} );

it( 'is false with no attributes', () => {
expect( isStoreMode( undefined ) ).toBe( false );
expect( isStoreMode( {} ) ).toBe( false );
} );
} );
65 changes: 65 additions & 0 deletions tests/unit/FlexAnalyticsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,71 @@ public function test_never_tracks_a_preview( $attributes ) {
$this->assertFalse( ceros_flex_tracking_enabled( $attributes, true ) );
}

/**
* @return array<string, array{0: array}>
*/
public static function store_mode_blocks() {
return [
'default' => [ self::stored() ],
'toggle on' => [ self::stored( [ 'cerosAnalytics' => true ] ) ],
'toggle off' => [ self::stored( [ 'cerosAnalytics' => false ] ) ],
];
}

/**
* An SSR block rendering from a stored copy.
*
* @param array $attributes Attributes to add.
* @return array
*/
private static function stored( $attributes = [] ) {
return array_merge(
[
'deliveryMode' => 'ssr',
'storedIndexPath' => 'ceros-flex/12/exp/v1/index.json',
],
$attributes
);
}

/**
* A stored copy has no reporting bundle, and analytics events aren't
* versioned, so Store mode never reports.
*
* @dataProvider store_mode_blocks
*/
public function test_never_tracks_in_store_mode( $attributes ) {
$this->assertFalse( ceros_flex_tracking_enabled( $attributes, false ) );
}

/**
* @return array<string, array{0: array, 1: bool}>
*/
public static function store_mode_cases() {
return [
'ssr with a stored copy' => [ self::stored(), true ],
'live ssr' => [ [ 'deliveryMode' => 'ssr' ], false ],
'ssr with an empty path' => [ self::stored( [ 'storedIndexPath' => '' ] ), false ],
'inline, stored copy left' => [ self::stored( [ 'deliveryMode' => 'inline' ] ), false ],
'iframe, stored copy left' => [ self::stored( [ 'deliveryMode' => 'iframe' ] ), false ],
'no delivery mode, stored' => [ [ 'storedIndexPath' => 'ceros-flex/12/exp/v1/index.json' ], false ],
];
}

/**
* A stored copy left on a block that has switched away from SSR is never
* rendered, so it doesn't turn tracking off.
*
* @dataProvider store_mode_cases
*/
public function test_detects_store_mode( $attributes, $expected ) {
$this->assertSame( $expected, ceros_flex_is_store_mode( $attributes ) );
}

public function test_tracks_after_switching_away_from_store_mode() {
$this->assertTrue( ceros_flex_tracking_enabled( self::stored( [ 'deliveryMode' => 'inline' ] ), false ) );
}

public function test_writes_enabled_when_tracking() {
$this->assertSame( 'enabled', ceros_flex_analytics_value( true ) );
}
Expand Down
Loading