Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e6e4774
Merge branch 'master' into uat
gabriel-detassigny Jul 20, 2026
111318f
Merge pull request #1205 from cloudinary/develop
gabrielcld2 Jul 20, 2026
48286c1
Merge pull request #1206 from cloudinary/uat
gabrielcld2 Jul 20, 2026
018fcc1
Pass version explicitly to the release GH action
gabriel-detassigny Jul 21, 2026
603fe2f
Instrument connection, sync, settings, media, cache, features, and de…
gabriel-detassigny Jul 31, 2026
9593d81
Rebuild JS bundles for the new analytics call sites
gabriel-detassigny Aug 3, 2026
385d11b
Merge remote-tracking branch 'origin/develop' into feature/custom-eve…
gabriel-detassigny Aug 3, 2026
330e67b
Add Analytics to get_component()'s return type for phpstan level 5
gabriel-detassigny Aug 3, 2026
0a787d2
Fix Image_Preview's placeholder src causing a self-fetch on every pag…
gabriel-detassigny Aug 4, 2026
ba269c6
Merge remote-tracking branch 'origin/develop' into feature/custom-eve…
gabriel-detassigny Aug 4, 2026
55fb9f3
Rebuild JS bundles for the new analytics call sites
gabriel-detassigny Aug 4, 2026
1531459
Continue analytics implementation
gabriel-detassigny Aug 6, 2026
c98e6d7
Automate sync category e2e coverage
gabriel-detassigny Aug 6, 2026
da72772
Automate media category e2e coverage
gabriel-detassigny Aug 6, 2026
1e1dd5a
Automate cache category e2e coverage
gabriel-detassigny Aug 6, 2026
72fd7ec
Automate features category e2e coverage
gabriel-detassigny Aug 6, 2026
f2db431
Add e2e coverage for connection_disconnected, deactivation_submitted,…
gabriel-detassigny Aug 6, 2026
5d0f0ac
Preempt real analytics/deactivation-feedback traffic from test runs; …
gabriel-detassigny Aug 10, 2026
cac149e
Make asset_cache_purged test deterministic instead of relying on page…
gabriel-detassigny Aug 10, 2026
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
13 changes: 10 additions & 3 deletions .github/workflows/deploy-to-wp-org.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,21 @@ jobs:
- name: Prepare build directory
run: npx grunt prepare

# Read once so both the tag check and the deploy step use the exact same value, rather
# than letting the deploy action derive its own version from GITHUB_REF - that only works
# when triggered from a tag push, and yields a bogus value like "refs/heads/master" (which
# SVN then can't use as a tag path) on a workflow_dispatch run against a branch.
- name: Read plugin version
run: echo "PLUGIN_VERSION=$(cat .version | tr -d '[:space:]')" >> "$GITHUB_ENV"

# Ensure the version in the .version file matches the tag of the release.
# Skipped for manual runs, which may not be run against a release tag.
- name: Verify version matches tag
if: github.event_name == 'release'
run: |
TAG="${GITHUB_REF_NAME#v}"
FILE_VERSION=$(cat .version | tr -d '[:space:]')
if [ "$TAG" != "$FILE_VERSION" ]; then
echo "::error::Tag $TAG does not match .version $FILE_VERSION"
if [ "$TAG" != "$PLUGIN_VERSION" ]; then
echo "::error::Tag $TAG does not match .version $PLUGIN_VERSION"
exit 1
fi

Expand All @@ -102,6 +108,7 @@ jobs:
env:
BUILD_DIR: 'build'
SLUG: 'cloudinary-image-management-and-manipulation-in-the-cloud-cdn'
VERSION: ${{ env.PLUGIN_VERSION }}
# Use secrets to authenticate with WP.org.
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
Expand Down
94 changes: 94 additions & 0 deletions .wp-env/mu-plugins/analytics-capture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
/**
* Analytics egress capture — local/e2e dev helper mu-plugin.
*
* Intercepts outgoing requests to any Cloudinary analytics-api.cloudinary.com
* endpoint — the custom-events collector AND the older deactivation-reason
* collector, both on the same host — and appends each payload as a JSONL
* entry to a log file, so e2e tests and manual QA can assert on emitted
* events. The request is fully preempted with a synthetic response: earlier
* versions of this mu-plugin only logged the payload and let the request
* proceed, which meant every local/CI test run was quietly leaking synthetic
* events (and deactivation "feedback") into the real production collector.
*
* @package Cloudinary
*/

defined( 'ABSPATH' ) || exit;

/**
* Returns the path to the capture log file.
*
* @return string
*/
function cld_analytics_capture_log_path() {
$upload = wp_upload_dir();

return $upload['basedir'] . '/analytics-capture.log';
}

add_filter( 'pre_http_request', 'cld_analytics_capture_intercept', 10, 3 );

/**
* Logs outgoing analytics/deactivation-reason requests and preempts them
* with a synthetic success response, so nothing actually reaches the real
* collector during local dev or CI runs.
*
* @param false|array|WP_Error $preempt Whether to preempt the request.
* @param array $parsed_args Parsed request arguments.
* @param string $url The request URL.
*
* @return false|array|WP_Error
*/
function cld_analytics_capture_intercept( $preempt, $parsed_args, $url ) {
if ( false === strpos( $url, 'analytics-api.cloudinary.com' ) ) {
return $preempt;
}

$body = isset( $parsed_args['body'] ) ? $parsed_args['body'] : '';
$decoded = json_decode( $body, true );

file_put_contents( // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_file_put_contents
cld_analytics_capture_log_path(),
wp_json_encode( null !== $decoded ? $decoded : $body ) . "\n",
FILE_APPEND | LOCK_EX
);

return array(
'headers' => array(),
'body' => '',
'response' => array(
'code' => 200,
'message' => 'OK',
),
'cookies' => array(),
'filename' => null,
);
}

/**
* Prints the captured analytics events, one JSON object per line.
*
* ## OPTIONS
*
* [--clear]
* : Empty the log after printing it.
*
* @param array $args Positional arguments.
* @param array $assoc_args Associative arguments.
*/
function cld_analytics_capture_wpcli_command( $args, $assoc_args ) {
$log_file = cld_analytics_capture_log_path();

if ( file_exists( $log_file ) ) {
WP_CLI::line( file_get_contents( $log_file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown
}

if ( isset( $assoc_args['clear'] ) ) {
file_put_contents( $log_file, '' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_file_put_contents
}
}

if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'cloudinary analytics-events', 'cld_analytics_capture_wpcli_command' );
}
2 changes: 1 addition & 1 deletion js/cloudinary.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/deactivate.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array(), 'version' => '41ae783fa52f94bc2630');
<?php return array('dependencies' => array('wp-api-fetch'), 'version' => 'e3528eb2e86ccc7789df');
2 changes: 1 addition & 1 deletion js/deactivate.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 80 additions & 0 deletions php/assets/class-rest-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use WP_REST_Response;
use WP_Error;
use WP_HTTP_Response;
use function Cloudinary\get_plugin_instance;

/**
* Class Rest Assets.
Expand Down Expand Up @@ -108,15 +109,30 @@ public function rest_save_asset( $request ) {
$media = $this->assets->media;
$attachment_id = $request->get_param( 'ID' );
$type = $media->get_resource_type( $attachment_id );
$analytics = get_plugin_instance()->get_component( 'analytics' );

$return = array();
$saved = false;

// Save transformations if present.
$transformations = $request->get_param( 'transformations' );

if ( isset( $transformations ) ) {
$result = $this->save_transformation_type( $attachment_id, $transformations, $type, 'transformations' );
$return = array_merge( $return, $result );
$saved = true;

if ( $analytics ) {
$analytics->track(
'transformation_applied',
'media',
null,
array(
'scope' => 'asset',
'transformation_count' => $this->count_transformation_qualifiers( $result['transformations'] ),
)
);
}
}

// Save text overlay even if empty (allow clearing).
Expand All @@ -125,6 +141,7 @@ public function rest_save_asset( $request ) {
if ( isset( $text_overlay ) && array_key_exists( 'transformation', (array) $text_overlay ) ) {
$result = $this->save_transformation_type( $attachment_id, $text_overlay['transformation'], $type, 'text_overlay', $text_overlay );
$return = array_merge( $return, $result );
$saved = true;
}

// Save image overlay even if empty (allow clearing).
Expand All @@ -133,11 +150,44 @@ public function rest_save_asset( $request ) {
if ( isset( $image_overlay ) && array_key_exists( 'transformation', (array) $image_overlay ) ) {
$result = $this->save_transformation_type( $attachment_id, $image_overlay['transformation'], $type, 'image_overlay', $image_overlay );
$return = array_merge( $return, $result );
$saved = true;
}

if ( $saved && $analytics ) {
$analytics->track(
'asset_edited',
'media',
null,
array(
'asset_id' => (int) $attachment_id,
'asset_type' => $type,
)
);
}

return rest_ensure_response( $return );
}

/**
* Counts the comma/slash-separated qualifiers in a transformation string.
*
* @param string $transformation The cleaned transformation string.
*
* @return int
*/
protected function count_transformation_qualifiers( $transformation ) {
if ( empty( $transformation ) ) {
return 0;
}

$count = 0;
foreach ( array_filter( explode( '/', $transformation ) ) as $segment ) {
$count += count( array_filter( explode( ',', $segment ) ) );
}

return $count;
}

/**
* Shared helper to save a transformation type (main, text overlay, image overlay).
*
Expand Down Expand Up @@ -222,6 +272,7 @@ public function rest_purge_all( $request ) {
$count = $request->get_param( 'count' );
$clean = $this->assets->clean_path( $parent_url );
$parent = $this->assets->get_param( $clean );
$analytics = get_plugin_instance()->get_component( 'analytics' );
$result = array(
'total' => 0,
'pending' => count( $this->assets->get_asset_parents() ),
Expand All @@ -244,6 +295,10 @@ public function rest_purge_all( $request ) {
$result['total'] = 0;
$result['pending'] = 0;
$result['percent'] = 100;

if ( $analytics ) {
$analytics->track( 'asset_cache_purged', 'cache', null, array( 'scope' => $clean ) );
}
} elseif ( false === $count ) {
$data = array(
'public_id' => null,
Expand All @@ -260,6 +315,10 @@ public function rest_purge_all( $request ) {
$result['total'] = 0;
$result['pending'] = 0;
$result['percent'] = 100;

if ( $analytics ) {
$analytics->track( 'all_cache_purged', 'cache' );
}
}

return rest_ensure_response( $result );
Expand All @@ -280,6 +339,11 @@ public function rest_get_caches( $request ) {
$current_page = $page ? $page : 1;
$data = $this->get_assets( $parent->ID, $search, $current_page );

$analytics = get_plugin_instance()->get_component( 'analytics' );
if ( $analytics ) {
$analytics->track( 'cache_items_viewed', 'cache', null, array( 'cache_point' => (string) $url ) );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache_items_viewed overcounts. This fires on every rest_get_caches() call, including each pagination step and each search keystroke-triggered request. If the spec intends "user viewed a cache point", consider tracking only page 1 / no-search requests.

}

return rest_ensure_response( $data );
}

Expand Down Expand Up @@ -308,6 +372,22 @@ public function rest_handle_state( $request ) {
global $wpdb;
$ids = $request['ids'];
$state = $request['state'];

if ( 'delete' !== $state ) {
$analytics = get_plugin_instance()->get_component( 'analytics' );
if ( $analytics ) {
$analytics->track(
'cache_items_toggled',
'cache',
null,
array(
'enabled' => 'enable' === strtolower( $state ),
'item_count' => count( $ids ),
)
);
}
}

foreach ( $ids as $id ) {
$where = array(
'post_id' => $id,
Expand Down
Loading
Loading