Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
29 changes: 23 additions & 6 deletions inc/class-registration.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,9 @@ public function enqueue_block_editor_assets() {

$is_wp_ai_backend = AI_Client_Adaptor::BACKEND_WP === AI_Client_Adaptor::resolve_backend();

wp_localize_script(
'otter-blocks',
'themeisleGutenberg',
array(
$can_track = 'yes' === get_option( 'otter_blocks_logger_flag', false );

$themeisle_gutenberg = array(
'hasNeve' => defined( 'NEVE_VERSION' ),
'hasPro' => Pro::is_pro_installed(),
'isProActive' => Pro::is_pro_active(),
Expand All @@ -309,7 +308,7 @@ public function enqueue_block_editor_assets() {
'themeDefaults' => Main::get_global_defaults(),
'imageSizes' => function_exists( 'is_wpcom_vip' ) ? array( 'thumbnail', 'medium', 'medium_large', 'large' ) : get_intermediate_image_sizes(), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_intermediate_image_sizes_get_intermediate_image_sizes
'isWPVIP' => function_exists( 'is_wpcom_vip' ),
'canTrack' => 'yes' === get_option( 'otter_blocks_logger_flag', false ) ? true : false,
'canTrack' => $can_track ? true : false,
'userRoles' => $wp_roles->roles,
'isBlockEditor' => 'post' === $current_screen->base,
'postTypes' => get_post_types(
Expand Down Expand Up @@ -343,7 +342,25 @@ public function enqueue_block_editor_assets() {
'connectorsUrl' => esc_url( admin_url( 'options-connectors.php' ) ),
'hasPatternSources' => Template_Cloud::has_used_pattern_sources(),
'proPatterns' => boolval( get_option( 'themeisle_blocks_settings_patterns_library', true ) ) ? Patterns::get_upsell_patterns() : array(),
)
);

if ( $can_track ) {
$themeisle_gutenberg['telemetry'] = array(
'loggerData' => get_option(
'otter_blocks_logger_data',
array(
'blocks' => array(),
'templates' => array(),
)
),
'firstSaveDone' => (bool) get_option( 'otter_activation_first_save', false ),
);
}

wp_localize_script(
'otter-blocks',
'themeisleGutenberg',
$themeisle_gutenberg
);

add_filter( 'themeisle-sdk/survey/' . OTTER_PRODUCT_SLUG, array( Dashboard::class, 'get_survey_metadata' ), 10, 2 );
Expand Down
11 changes: 11 additions & 0 deletions inc/plugins/class-options-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,17 @@ public function register_settings() {
)
);

register_setting(
'themeisle_blocks_settings',
'otter_activation_first_save',
array(
'type' => 'boolean',
'description' => __( 'Whether the activation first-save milestone has already been tracked for this site.', 'otter-blocks' ),
'show_in_rest' => true,
'default' => false,
)
);

register_setting(
'themeisle_blocks_settings',
'themeisle_google_map_block_api_key',
Expand Down
3 changes: 3 additions & 0 deletions packages/e2e-tests/mu-plugins/otter-e2e-bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
'themeisle_blocks_form_fields_option',
'themeisle_blocks_settings_patterns_library',
'themeisle_blocks_settings_atomic_wind_blocks',
'otter_blocks_logger_flag',
'otter_blocks_logger_data',
'otter_activation_first_save',
);

/**
Expand Down
11 changes: 9 additions & 2 deletions src/blocks/blocks/content-generator/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import PreviewBoundary from './preview-boundary.js';
import PromptPlaceholder from '../../components/prompt';
import { aiGeneration as icon } from '../../helpers/icons.js';
import { parseFormPromptResponseToBlocks } from '../../helpers/prompt';
import AIContentModal from '../../plugins/ai-content/modal';
import AIContentModal, { trackAiEvent } from '../../plugins/ai-content/modal';
import EnableAtomicWind from '../../plugins/patterns-library/enableAtomicWind';
import useSettings from '../../helpers/use-settings';
import {
Expand Down Expand Up @@ -170,6 +170,8 @@ const ContentGenerator = ({
} else {
replaceBlocks( clientId, blocksToInsert );
}

trackAiEvent( `ai-outcome-${ clientId }`, 'outcome-form', 'replace' );
};

/**
Expand Down Expand Up @@ -246,7 +248,12 @@ const ContentGenerator = ({
onValueChange={ setPrompt }
onPreview={ onPreview }
actionButtons={ actionButtons }
onClose={ () => removeBlock( clientId ) }
onClose={ () => {
if ( hasInnerBlocks ) {
trackAiEvent( `ai-outcome-${ clientId }`, 'outcome-form', 'discard' );
}
removeBlock( clientId );
} }
promptPlaceholder={ __( 'Start describing what form you need…', 'otter-blocks' ) }
>
{
Expand Down
37 changes: 37 additions & 0 deletions src/blocks/plugins/ai-content/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ import { useAtomicCssForContent } from '../patterns-library/atomic';

const EMPTY_PREVIEW_BLOCKS: BlockProps<unknown>[] = [];

export const trackAiEvent = ( key: string, featureComponent: string, featureValue: string ) =>
window.oTrk?.set( key, { feature: 'ai-generation', featureComponent, featureValue });

// Bucket the regeneration count into a coarse, non-PII enum for telemetry.
const retryBucket = ( count: number ): string => {
if ( 1 > count ) {
return '0';
}
if ( 1 === count ) {
return '1';
}
if ( 4 > count ) {
return '2-3';
}
return '4+';
};

/**
* Cheap stable hash so identical preview markup reuses its generated CSS.
* @param value
Expand Down Expand Up @@ -165,6 +182,10 @@ const AIContentModal = ({
const isCreateMode = 'create' === mode;
const scope = initialScope;

const trackingKey = singleClientId ?? selectedClientIds?.[0] ?? 'ai-modal';
const trackOutcome = ( featureValue: string ) =>
trackAiEvent( `ai-outcome-${ trackingKey }`, isCreateMode ? 'outcome-create' : 'outcome-transform', featureValue );

const [ pinnedPreviewClone, setPinnedPreviewClone ] = useState<BlockProps<unknown>[]>( EMPTY_PREVIEW_BLOCKS );
const wasOpenRef = useRef( false );

Expand Down Expand Up @@ -194,6 +215,14 @@ const AIContentModal = ({
isMultipleSelection ? selectedClientIds.join( ',' ) : singleClientId
]);

useEffect( () => {
if ( isOpen ) {
return () => {
window.oTrk?.base?.uploadEvents();
};
}
}, [ isOpen ]);

const hasSelection = 0 < pinnedPreviewClone.length;

const [ getOption ] = useSettings();
Expand Down Expand Up @@ -673,6 +702,8 @@ const AIContentModal = ({
)
}, { consent: true });

trackAiEvent( `ai-retries-${ trackingKey }`, 'regenerate-count', retryBucket( priorTurns.length ) );

if ( isStale() ) {
return;
}
Expand Down Expand Up @@ -817,6 +848,7 @@ const AIContentModal = ({

try {
if ( onApplyBlocks ) {
trackOutcome( 'insert' );
onApplyBlocks( blocksToInsert );
return;
}
Expand All @@ -831,6 +863,7 @@ const AIContentModal = ({
}

replaceBlocks( replaceClientIds, blocksToInsert );
trackOutcome( 'replace' );
( onApplyComplete ?? onClose )();
} catch {
createNotice(
Expand Down Expand Up @@ -875,6 +908,10 @@ const AIContentModal = ({
return;
}

if ( hasRealTurns ) {
trackOutcome( 'discard' );
}

( onDiscard ?? onClose )();
};

Expand Down
106 changes: 106 additions & 0 deletions src/blocks/plugins/data-logging/activation-funnel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Tracks first Otter block insert and first save activation milestones.
*/

/**
* WordPress dependencies
*/
import { select } from '@wordpress/data';

import {
countTrackedBlocks,
getDepthBucket,
persistFlag
} from './shared.js';

/** @type {{ firstSaveDone: boolean, firstInsertDone: boolean }|null} */
let state = null;

/** @type {number|null} Baseline tracked-block count at first stable editor tick. */
let insertBaseline = null;

const trackFirstInsert = () => {
state.firstInsertDone = true;

window.oTrk?.set( 'activation-first-insert', {
feature: 'activation',
featureComponent: 'first-insert',
featureValue: 'true'
});
};

const trackFirstSave = async otterBlockCount => {
state.firstSaveDone = true;

window.oTrk?.set( 'activation-first-save', {
feature: 'activation',
featureComponent: 'first-save',
featureValue: 'true'
});

window.oTrk?.set( 'activation-first-save-depth', {
feature: 'activation',
featureComponent: 'first-save-depth',
featureValue: getDepthBucket( otterBlockCount )
});

await persistFlag( 'otter_activation_first_save' );
};

const captureInsertBaselineIfReady = () => {
if ( null !== insertBaseline || ! state ) {
return;
}

const { __unstableIsEditorReady } = select( 'core/editor' );

if ( __unstableIsEditorReady?.() ) {
insertBaseline = countTrackedBlocks();
}
};

/**
* @param {{ firstSaveDone: boolean, firstInsertDone: boolean }} telemetryState Bootstrap state.
*/
export const startActivation = telemetryState => {
state = telemetryState;
insertBaseline = null;
captureInsertBaselineIfReady();
};

/**
* Handle editor store ticks for activation milestones.
*/
export const onActivationEditorTick = () => {
if ( ! state ) {
return;
}

const {
isAutosavingPost,
isSavingPost
} = select( 'core/editor' );

captureInsertBaselineIfReady();

if ( isSavingPost() && ! isAutosavingPost() && ! state.firstSaveDone ) {
const otterBlockCount = countTrackedBlocks();

if ( 0 < otterBlockCount ) {
trackFirstSave( otterBlockCount );
}
}
};

/**
* Detect a real Otter block insertion after the baseline was captured.
*/
export const onActivationBlockTick = () => {
if ( ! state || null === insertBaseline || state.firstInsertDone ) {
return;
}

if ( countTrackedBlocks() > insertBaseline ) {
trackFirstInsert();
}
};
91 changes: 91 additions & 0 deletions src/blocks/plugins/data-logging/block-health.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Tracks Otter blocks that fail to render in the editor.
*/

/**
* External dependencies
*/
import { debounce } from 'lodash';

/**
* WordPress dependencies
*/
import { select, subscribe } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';

import { getTrackedBlockNames } from './shared.js';

// ponytail: session-only dedup; sessionStorage if reopen inflation matters.
const reportedSlugs = new Set();

/**
* Walk the block tree and collect slugs for broken Otter blocks.
*
* @param {Array} blocks Blocks to inspect.
* @param {string[]} tracked Otter block names to watch.
* @param {Set<string>} acc Accumulator for errored slugs.
* @return {Set<string>}
*/
const collectErroredSlugs = ( blocks, tracked, acc ) => {
blocks.forEach( block => {
if ( false === block.isValid && tracked.includes( block.name ) ) {
acc.add( block.name );
}

if ( 'core/missing' === block.name ) {
const originalName = block.attributes?.originalName;

if ( originalName && tracked.includes( originalName ) ) {
acc.add( originalName );
}
}

if ( block.innerBlocks?.length ) {
collectErroredSlugs( block.innerBlocks, tracked, acc );
}
});

return acc;
};

/**
* Report newly detected broken Otter blocks.
*/
const checkBlockHealth = () => {
const trackedBlockNames = getTrackedBlockNames();

if ( 0 === trackedBlockNames.length ) {
return;
}

const { getBlocks } = select( blockEditorStore );
const errored = collectErroredSlugs(
getBlocks(),
trackedBlockNames,
new Set()
);

errored.forEach( slug => {
if ( reportedSlugs.has( slug ) ) {
return;
}

reportedSlugs.add( slug );

window.oTrk?.set( `block-health-${ slug }`, {
feature: 'block-health',
featureComponent: 'render-error',
featureValue: slug
});
});
};

export const startBlockHealth = () => {
if ( ! select( blockEditorStore ) || ! select( 'core/blocks' ) ) {
return;
}

setTimeout( () => {
subscribe( debounce( checkBlockHealth, 1000 ), blockEditorStore );
}, 1000 );
};
Loading
Loading