-
Notifications
You must be signed in to change notification settings - Fork 36
Wire activation-funnel analytics events #1189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,6 +64,143 @@ public function __construct( Plugin $plugin ) { | |
| add_filter( 'cloudinary_api_rest_endpoints', array( $this, 'rest_endpoints' ) ); | ||
| add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_script_data' ) ); | ||
| add_action( 'admin_init', array( $this, 'maybe_send_smoke_event' ) ); | ||
| add_action( 'admin_init', array( $this, 'maybe_send_pending_activation' ) ); | ||
| add_action( 'cloudinary_uploaded_asset', array( $this, 'maybe_first_api_consumption' ), 10, 2 ); | ||
| } | ||
|
|
||
| /** | ||
| * Option/transient keys used by the activation funnel. | ||
| */ | ||
| const PENDING_ACTIVATION = '_cloudinary_pending_activation'; | ||
| const LAST_ACTIVE = '_cloudinary_last_active'; | ||
| const FIRST_API_FLAG = '_cloudinary_first_api_emitted'; | ||
|
|
||
| /** | ||
| * Records the activation type on plugin activation (funnel step 1). | ||
| * | ||
| * Runs from the activation hook (`Utils::install`). Detects fresh install / | ||
| * reactivation / upgrade / downgrade from the persisted install marker | ||
| * (`db_version`) vs. the current version, then stashes a transient that the | ||
| * next admin load turns into a `plugin_activated` event — by which point the | ||
| * full mandatory params and `session_id` are available. | ||
| * | ||
| * @return void | ||
| */ | ||
| public static function stash_activation() { | ||
| try { | ||
| $current = get_plugin_instance()->version; | ||
| $db_version = get_option( Sync::META_KEYS['db_version'] ); | ||
|
|
||
| if ( empty( $db_version ) ) { | ||
| $type = 'fresh_install'; | ||
| $previous = null; | ||
| } elseif ( version_compare( $db_version, $current, '<' ) ) { | ||
| $type = 'upgrade'; | ||
| $previous = $db_version; | ||
| } elseif ( version_compare( $db_version, $current, '>' ) ) { | ||
| $type = 'downgrade'; | ||
| $previous = $db_version; | ||
| } else { | ||
| $type = 'reactivation'; | ||
| $previous = $db_version; | ||
| } | ||
|
|
||
| $days_since_last_active = null; | ||
| if ( 'reactivation' === $type ) { | ||
| $last = (int) get_option( self::LAST_ACTIVE ); | ||
| if ( $last > 0 ) { | ||
| $days_since_last_active = (int) floor( ( time() - $last ) / DAY_IN_SECONDS ); | ||
| } | ||
| } | ||
|
|
||
| set_transient( | ||
| self::PENDING_ACTIVATION, | ||
| array( | ||
| 'activation_type' => $type, | ||
| 'previous_version' => $previous, | ||
| 'new_version' => $current, | ||
| 'days_since_last_active' => $days_since_last_active, | ||
| ), | ||
| HOUR_IN_SECONDS | ||
| ); | ||
| } catch ( \Throwable $e ) { | ||
| // Fail silent: activation must never break. | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Persists a last-active timestamp on deactivation. | ||
| * | ||
| * Feeds `days_since_last_active` on the next reactivation. Runs from the | ||
| * deactivation hook. | ||
| * | ||
| * @return void | ||
| */ | ||
| public static function record_deactivation() { | ||
| update_option( self::LAST_ACTIVE, time(), false ); | ||
| } | ||
|
|
||
| /** | ||
| * Emits the stashed `plugin_activated` event on the next admin load. | ||
| * | ||
| * @return void | ||
| */ | ||
| public function maybe_send_pending_activation() { | ||
| $pending = get_transient( self::PENDING_ACTIVATION ); | ||
| if ( empty( $pending ) || ! is_array( $pending ) ) { | ||
| return; | ||
| } | ||
| delete_transient( self::PENDING_ACTIVATION ); | ||
|
|
||
| $params = array( | ||
| 'activation_type' => $pending['activation_type'], | ||
| 'new_version' => $pending['new_version'], | ||
| ); | ||
| if ( ! empty( $pending['previous_version'] ) ) { | ||
| $params['previous_version'] = $pending['previous_version']; | ||
| } | ||
| if ( isset( $pending['days_since_last_active'] ) && null !== $pending['days_since_last_active'] ) { | ||
| $params['days_since_last_active'] = $pending['days_since_last_active']; | ||
| } | ||
|
|
||
| $this->track( 'plugin_activated', 'activation_funnel', 1, $params ); | ||
| } | ||
|
|
||
| /** | ||
| * Emits the one-time `first_api_consumption` activation marker (funnel step 9). | ||
| * | ||
| * Hooked to `cloudinary_uploaded_asset`, which fires after an asset upload. | ||
| * Emitted once on the first successful upload, then suppressed. | ||
| * | ||
| * @param int $attachment_id The attachment ID. | ||
| * @param array|\WP_Error $result The upload result. | ||
| * | ||
| * @return void | ||
| */ | ||
| public function maybe_first_api_consumption( $attachment_id, $result ) { | ||
| if ( empty( $result ) || is_wp_error( $result ) ) { | ||
| return; | ||
| } | ||
| if ( get_option( self::FIRST_API_FLAG ) ) { | ||
| return; | ||
| } | ||
| update_option( self::FIRST_API_FLAG, true, false ); | ||
|
|
||
| $asset_type = ''; | ||
| if ( is_array( $result ) && ! empty( $result['resource_type'] ) ) { | ||
| $asset_type = $result['resource_type']; | ||
| } | ||
|
|
||
| $this->track( | ||
| 'first_api_consumption', | ||
| 'activation_funnel', | ||
| 9, | ||
| array( | ||
| 'api_endpoint' => 'upload', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re Same §2.3 issue from the bridge: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing Also |
||
| 'asset_type' => $asset_type, | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -153,6 +153,21 @@ public function rest_test_connection( WP_REST_Request $request ) { | |
| $url = $request->get_param( 'cloudinary_url' ); | ||
| $result = $this->test_connection( $url ); | ||
|
|
||
| $analytics = $this->plugin->get_component( 'analytics' ); | ||
| if ( $analytics ) { | ||
| $success = 'connection_success' === $result['type']; | ||
| $analytics->track( | ||
| 'connection_test_result', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Also L165 reads |
||
| 'activation_funnel', | ||
| 3, | ||
| array( | ||
| 'status' => $success ? 'success' : 'error', | ||
| 'error_type' => $success ? '' : $result['type'], | ||
| 'attempt_number' => (int) $request->get_param( 'attempt_number' ), | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| return rest_ensure_response( $result ); | ||
| } | ||
|
|
||
|
|
@@ -224,6 +239,21 @@ public function rest_save_wizard( WP_REST_Request $request ) { | |
| ); | ||
| } | ||
|
|
||
| $analytics = $this->plugin->get_component( 'analytics' ); | ||
| if ( $analytics ) { | ||
| $analytics->track( | ||
| 'wizard_setup_submitted', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 'activation_funnel', | ||
| 5, | ||
| array( | ||
| 'media_library' => 'on' === $media, | ||
| 'non_media' => 'on' === $nonmedia, | ||
| 'advanced' => 'on' === $advanced, | ||
| 'status' => 'success', | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| return rest_ensure_response( $this->settings->get_value() ); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -201,6 +201,18 @@ public function process_assets( $attachments = array() ) { | |
| // If a single specified ID, push and return response. | ||
| $ids = array_map( 'intval', (array) $attachments ); | ||
| $thread = $this->plugin->settings->get_param( 'current_sync_thread' ); | ||
|
|
||
| // Activation funnel: first sync started (emitted once). | ||
| $analytics = $this->plugin->get_component( 'analytics' ); | ||
| if ( $analytics && ! get_option( '_cloudinary_first_sync_emitted' ) ) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The flag is also global, not per-account — spec defines this as one-time per account, so a second cloud never re-emits. Same for Also confirm |
||
| update_option( '_cloudinary_first_sync_emitted', true, false ); | ||
| $analytics->track( | ||
| 'first_sync_started', | ||
| 'activation_funnel', | ||
| 8, | ||
| array( 'asset_count' => count( $ids ) ) | ||
| ); | ||
| } | ||
| // Handle based on Sync Type. | ||
| foreach ( $ids as $attachment_id ) { | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re
php/class-analytics.php:306—'is_multisite' => is_multisite()in thetrack()send body (anchored here; L306 is outside this PR's diff)Spec §2.3 violation:
is_multisiteis a PHP bool placed in thewp_remote_postbody array, so WP serializesfalse→""andtrue→"1". Spec §2.3 requires real booleans.Recommend JSON-encoding the body so this — plus
format_valid,enabled, and thewizard_setup_submittedflags — serialize as true JSON booleans.