From ad0c21f7270c0875a012cb391f54ad8c2c53b0f0 Mon Sep 17 00:00:00 2001 From: strausr Date: Thu, 5 Feb 2026 10:12:01 -0800 Subject: [PATCH 1/4] feat(analytics): add CLI feature detection for React SDK --- packages/html/src/types.ts | 2 +- packages/html/src/utils/analytics.ts | 1 + .../react/src/internal/SDKAnalyticsConstants.ts | 14 +++++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/html/src/types.ts b/packages/html/src/types.ts index 7500bf08..a9a895e4 100644 --- a/packages/html/src/types.ts +++ b/packages/html/src/types.ts @@ -24,7 +24,7 @@ export type PictureSources = {minWidth?: number, maxWidth?: number, image: Cloud export type PictureSource = {minWidth?: number, maxWidth?: number, image: CloudinaryImage, sizes?: string}; -export type BaseAnalyticsOptions = {sdkSemver: string, techVersion: string, sdkCode: string}; +export type BaseAnalyticsOptions = {sdkSemver: string, techVersion: string, sdkCode: string, feature?: string}; export type AnalyticsOptions = Parameters[0]; diff --git a/packages/html/src/utils/analytics.ts b/packages/html/src/utils/analytics.ts index cc052398..ae6b6a2d 100644 --- a/packages/html/src/utils/analytics.ts +++ b/packages/html/src/utils/analytics.ts @@ -6,6 +6,7 @@ export const getAnalyticsOptions = (options?: BaseAnalyticsOptions, features: vo sdkCode: options.sdkCode, sdkSemver: options.sdkSemver, techVersion: options.techVersion, + ...(options.feature !== undefined && { feature: options.feature }), ...features } } : null diff --git a/packages/react/src/internal/SDKAnalyticsConstants.ts b/packages/react/src/internal/SDKAnalyticsConstants.ts index 14fd72de..754c7869 100644 --- a/packages/react/src/internal/SDKAnalyticsConstants.ts +++ b/packages/react/src/internal/SDKAnalyticsConstants.ts @@ -1,7 +1,19 @@ import React from 'react' +// Detect if this project was created via create-cloudinary-react CLI +function getCLIFeatureCode(): string { + if (typeof process !== 'undefined' && process.env) { + if (process.env.CLOUDINARY_SOURCE === 'cli' || process.env.CLD_CLI === 'true') { + return 'B'; // CLI feature code + } + } + return '0'; // Default (no specific feature) +} + + export const SDKAnalyticsConstants = { sdkSemver: 'PACKAGE_VERSION_INJECTED_DURING_BUILD', techVersion: React.version, - sdkCode: 'J' + sdkCode: 'J', + feature: getCLIFeatureCode() }; From 896e26ae016ce23aad4bacc861705490ff2999a7 Mon Sep 17 00:00:00 2001 From: strausr Date: Mon, 9 Feb 2026 07:42:31 -0800 Subject: [PATCH 2/4] feat(analytics): add test to detect cli --- packages/html/src/types.ts | 2 +- packages/html/src/utils/analytics.ts | 1 + packages/react/__tests__/analytics.test.tsx | 37 +++++++++++++++++++ .../src/internal/SDKAnalyticsConstants.ts | 16 ++++---- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/packages/html/src/types.ts b/packages/html/src/types.ts index a9a895e4..79b0c56d 100644 --- a/packages/html/src/types.ts +++ b/packages/html/src/types.ts @@ -24,7 +24,7 @@ export type PictureSources = {minWidth?: number, maxWidth?: number, image: Cloud export type PictureSource = {minWidth?: number, maxWidth?: number, image: CloudinaryImage, sizes?: string}; -export type BaseAnalyticsOptions = {sdkSemver: string, techVersion: string, sdkCode: string, feature?: string}; +export type BaseAnalyticsOptions = {sdkSemver: string, techVersion: string, sdkCode: string, feature?: string, product?: string}; export type AnalyticsOptions = Parameters[0]; diff --git a/packages/html/src/utils/analytics.ts b/packages/html/src/utils/analytics.ts index ae6b6a2d..1dcaddb3 100644 --- a/packages/html/src/utils/analytics.ts +++ b/packages/html/src/utils/analytics.ts @@ -7,6 +7,7 @@ export const getAnalyticsOptions = (options?: BaseAnalyticsOptions, features: vo sdkSemver: options.sdkSemver, techVersion: options.techVersion, ...(options.feature !== undefined && { feature: options.feature }), + ...(options.product !== undefined && { product: options.product }), ...features } } : null diff --git a/packages/react/__tests__/analytics.test.tsx b/packages/react/__tests__/analytics.test.tsx index 96f468d6..f0bf1d83 100644 --- a/packages/react/__tests__/analytics.test.tsx +++ b/packages/react/__tests__/analytics.test.tsx @@ -26,3 +26,40 @@ describe('analytics', () => { }, 0);// one tick }); }); + +describe('analytics when created via CLI', () => { + let AdvancedImageCLI: typeof AdvancedImage; + let CloudinaryImageCLI: typeof CloudinaryImage; + + beforeAll(() => { + process.env.CLOUDINARY_SOURCE = 'cli'; + jest.resetModules(); + const src = require('../src'); + const constants = require('../src/internal/SDKAnalyticsConstants'); + AdvancedImageCLI = src.AdvancedImage; + CloudinaryImageCLI = require('@cloudinary/url-gen/assets/CloudinaryImage').CloudinaryImage; + const SDKAnalyticsConstantsCLI = constants.SDKAnalyticsConstants; + SDKAnalyticsConstantsCLI.sdkSemver = '1.0.0'; + SDKAnalyticsConstantsCLI.techVersion = '10.2.5'; + }); + + afterAll(() => { + delete process.env.CLOUDINARY_SOURCE; + jest.resetModules(); + }); + + it('generates analytics with Product B (Integrations) and sdkCode H (React CLI)', function (done) { + const cldImg = new CloudinaryImageCLI('sample', { cloudName: 'demo' }); + const component = mount(); + setTimeout(() => { + const html = component.html(); + const match = html.match(/_a=([A-Za-z0-9]+)/); + expect(match).toBeTruthy(); + const token = match![1]; + // Algorithm B: 1st = algo, 2nd = product (B = Integrations), 3rd = sdkCode (H = React CLI) + expect(token[1]).toBe('B'); + expect(token[2]).toBe('H'); + done(); + }, 0); + }); +}); diff --git a/packages/react/src/internal/SDKAnalyticsConstants.ts b/packages/react/src/internal/SDKAnalyticsConstants.ts index 754c7869..a3e68239 100644 --- a/packages/react/src/internal/SDKAnalyticsConstants.ts +++ b/packages/react/src/internal/SDKAnalyticsConstants.ts @@ -1,19 +1,19 @@ import React from 'react' -// Detect if this project was created via create-cloudinary-react CLI -function getCLIFeatureCode(): string { +// Detect if this project was created via create-cloudinary-react CLI (Integrations) +function isCLI(): boolean { if (typeof process !== 'undefined' && process.env) { - if (process.env.CLOUDINARY_SOURCE === 'cli' || process.env.CLD_CLI === 'true') { - return 'B'; // CLI feature code - } + return process.env.CLOUDINARY_SOURCE === 'cli' || process.env.CLD_CLI === 'true'; } - return '0'; // Default (no specific feature) + return false; } +// When CLI: use Algorithm B with Product B (Integrations) and sdkCode H (React CLI). Otherwise React SDK: sdkCode J. +const isCLIDetected = isCLI(); export const SDKAnalyticsConstants = { sdkSemver: 'PACKAGE_VERSION_INJECTED_DURING_BUILD', techVersion: React.version, - sdkCode: 'J', - feature: getCLIFeatureCode() + sdkCode: isCLIDetected ? 'H' : 'J', + ...(isCLIDetected && { product: 'B' as const }) }; From 8634d6e93720e116b9cf5c39710441b0e2eabeec Mon Sep 17 00:00:00 2001 From: strausr Date: Mon, 9 Feb 2026 07:51:12 -0800 Subject: [PATCH 3/4] feat(analytics): remove feature extension --- packages/html/src/types.ts | 2 +- packages/html/src/utils/analytics.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/html/src/types.ts b/packages/html/src/types.ts index 79b0c56d..6f9ba5f9 100644 --- a/packages/html/src/types.ts +++ b/packages/html/src/types.ts @@ -24,7 +24,7 @@ export type PictureSources = {minWidth?: number, maxWidth?: number, image: Cloud export type PictureSource = {minWidth?: number, maxWidth?: number, image: CloudinaryImage, sizes?: string}; -export type BaseAnalyticsOptions = {sdkSemver: string, techVersion: string, sdkCode: string, feature?: string, product?: string}; +export type BaseAnalyticsOptions = {sdkSemver: string, techVersion: string, sdkCode: string, product?: string}; export type AnalyticsOptions = Parameters[0]; diff --git a/packages/html/src/utils/analytics.ts b/packages/html/src/utils/analytics.ts index 1dcaddb3..ac4a4c5f 100644 --- a/packages/html/src/utils/analytics.ts +++ b/packages/html/src/utils/analytics.ts @@ -6,7 +6,6 @@ export const getAnalyticsOptions = (options?: BaseAnalyticsOptions, features: vo sdkCode: options.sdkCode, sdkSemver: options.sdkSemver, techVersion: options.techVersion, - ...(options.feature !== undefined && { feature: options.feature }), ...(options.product !== undefined && { product: options.product }), ...features } From 2df5c20b8a1299758233bf5ca5f517d70365aae4 Mon Sep 17 00:00:00 2001 From: strausr Date: Mon, 9 Feb 2026 08:04:46 -0800 Subject: [PATCH 4/4] fix(analytics): fixed broken test --- .../tests/cloudinary-image.component.spec.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/angular/projects/cloudinary-library/src/tests/cloudinary-image.component.spec.ts b/packages/angular/projects/cloudinary-library/src/tests/cloudinary-image.component.spec.ts index b4e91f38..7aefd3a3 100644 --- a/packages/angular/projects/cloudinary-library/src/tests/cloudinary-image.component.spec.ts +++ b/packages/angular/projects/cloudinary-library/src/tests/cloudinary-image.component.spec.ts @@ -52,21 +52,28 @@ describe('CloudinaryImageComponent render', () => { tick(0); const imgElement: HTMLImageElement = fixture.nativeElement; const img = imgElement.querySelector('img'); - expect(img.outerHTML).toBe('text text text'); + expect(img.getAttribute('alt')).toBe('text text text'); + expect(img.getAttribute('width')).toBe('400px'); + expect(img.getAttribute('height')).toBe('500px'); + expect(img.getAttribute('loading')).toBe('eager'); + expect(img.src).toBe('https://res.cloudinary.com/demo/image/upload/sample'); component.width = '800px'; component.alt = 'updated alt text'; component.height = '1000px'; component.loading = 'lazy'; component.ngOnChanges(); - expect(img.outerHTML).toBe('updated alt text'); + expect(img.getAttribute('alt')).toBe('updated alt text'); + expect(img.getAttribute('width')).toBe('800px'); + expect(img.getAttribute('height')).toBe('1000px'); + expect(img.getAttribute('loading')).toBe('lazy'); + expect(img.src).toBe('https://res.cloudinary.com/demo/image/upload/sample'); component.width = undefined; component.height = undefined; component.alt = ''; component.loading = 'lazy'; component.ngOnChanges(); - expect(img.outerHTML).toBe(''); + expect(img.getAttribute('alt')).toBe(''); + expect(img.getAttribute('loading')).toBe('lazy'); + expect(img.src).toBe('https://res.cloudinary.com/demo/image/upload/sample'); })); });