Skip to content
Draft
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
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
79 changes: 79 additions & 0 deletions .wp-env/mu-plugins/analytics-capture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php
/**
* Analytics egress capture — local/e2e dev helper mu-plugin.
*
* Intercepts outgoing POSTs to the Cloudinary custom-events collector
* (analytics-api.cloudinary.com) and appends each payload as a JSONL entry
* to a log file, so e2e tests and manual QA can assert on emitted events
* without hitting the real 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 events, letting the request proceed unmodified.
*
* @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 $preempt;
}

/**
* 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 ) );
}

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