diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 05e112a33c4..fc916f71f4f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1188,4 +1188,9 @@ export interface FeatureToggles { * Enable TTL plugin instance manager */ ttlPluginInstanceManager?: boolean; + /** + * Enables the new version of rudderstack + * @default false + */ + rudderstackUpgrade?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 1f7f3f9bdd5..ad87d0ec4cf 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -2064,6 +2064,18 @@ var ( FrontendOnly: true, Owner: grafanaPluginsPlatformSquad, }, + { + Name: "rudderstackUpgrade", + Description: "Enables the new version of rudderstack", + FrontendOnly: true, + Stage: FeatureStageExperimental, + Owner: grafanaFrontendPlatformSquad, + Expression: "false", + RequiresRestart: false, + AllowSelfServe: false, + HideFromDocs: false, + HideFromAdminPage: false, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 54a34b2e0bf..a19733d60c3 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -265,3 +265,4 @@ kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false awsDatasourcesHttpProxy,experimental,@grafana/aws-datasources,false,false,false transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true ttlPluginInstanceManager,experimental,@grafana/plugins-platform-backend,false,false,true +rudderstackUpgrade,experimental,@grafana/grafana-frontend-platform,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 2b31dcebfaa..412c678da0f 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -1069,4 +1069,8 @@ const ( // FlagTtlPluginInstanceManager // Enable TTL plugin instance manager FlagTtlPluginInstanceManager = "ttlPluginInstanceManager" + + // FlagRudderstackUpgrade + // Enables the new version of rudderstack + FlagRudderstackUpgrade = "rudderstackUpgrade" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index bb127ff7d26..e201c3bf1fa 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3662,6 +3662,20 @@ "codeowner": "@grafana/identity-access-team" } }, + { + "metadata": { + "name": "rudderstackUpgrade", + "resourceVersion": "1763481217872", + "creationTimestamp": "2025-11-18T15:53:37Z" + }, + "spec": { + "description": "Enables the new version of rudderstack", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true, + "expression": "false" + } + }, { "metadata": { "name": "savedQueries", diff --git a/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts b/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts index 03ea4baef00..691c33b4255 100644 --- a/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts +++ b/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts @@ -21,7 +21,21 @@ interface RudderstackAPIOptions { interface Rudderstack { identify: (identifier: string, traits: Properties, options?: RudderstackAPIOptions) => void; - load: (writeKey: string, dataPlaneURL: string, options: { configUrl?: string; destSDKBaseURL?: string }) => void; + // load type set to match Rudderstack v3, for global type compatibility with new version. + load: ( + writeKey: string, + dataPlaneURL: string, + options: { + configUrl?: string; + destSDKBaseURL?: string; + storage?: { + encryption?: { + version: 'V3' | 'legacy'; + }; + migrate?: boolean; + }; + } + ) => void; page: () => void; track: (eventName: string, properties?: Properties) => void; } diff --git a/public/app/core/services/echo/backends/analytics/RudderstackV3Backend.ts b/public/app/core/services/echo/backends/analytics/RudderstackV3Backend.ts new file mode 100644 index 00000000000..dd2c9a1513c --- /dev/null +++ b/public/app/core/services/echo/backends/analytics/RudderstackV3Backend.ts @@ -0,0 +1,166 @@ +import { BuildInfo } from '@grafana/data'; +import { + EchoBackend, + EchoEventType, + isExperimentViewEvent, + isInteractionEvent, + isPageviewEvent, + PageviewEchoEvent, +} from '@grafana/runtime'; + +import { User } from '../../../context_srv'; +import { loadScript } from '../../utils'; + +type Properties = Record; + +interface RudderstackAPIOptions { + Intercom?: { + user_hash: string; + }; +} + +interface Rudderstack { + identify: (identifier: string, traits: Properties, options?: RudderstackAPIOptions) => void; + load: ( + writeKey: string, + dataPlaneURL: string, + options: { + configUrl?: string; + destSDKBaseURL?: string; + storage?: { + encryption?: { + version: 'V3' | 'legacy'; + }; + migrate?: boolean; + }; + } + ) => void; + page: () => void; + track: (eventName: string, properties?: Properties) => void; +} + +declare global { + interface Window { + // We say all methods are undefined because we can't be sure they're there + // and we should be extra cautious + rudderanalytics?: Partial; + } +} + +export interface RudderstackBackendOptions { + writeKey: string; + dataPlaneUrl: string; + buildInfo: BuildInfo; + user?: User; + sdkUrl?: string; + configUrl?: string; + integrationsUrl?: string; +} + +export class RudderstackBackend implements EchoBackend { + supportedEvents = [EchoEventType.Pageview, EchoEventType.Interaction, EchoEventType.ExperimentView]; + + constructor(public options: RudderstackBackendOptions) { + const url = options.sdkUrl || `https://cdn.rudderlabs.com/v3/modern/rsa.min.js`; + loadScript(url); + + // this preloadRudderstack functionality is mirror the loading snippet reference in the Rudderstack + // docs here: https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/installation/#using-cdn + // We don't need a bunch of what's in the snippet, and we can rely on browser support being modern, + // so this is much more terse and only needs to handle the temp setup that pushes calls to the + // rudderstack API into the temp array. + const preloadRudderstack: unknown[] = []; + + const methodNames = [ + 'setDefaultInstanceKey', + 'load', + 'ready', + 'page', + 'track', + 'identify', + 'alias', + 'group', + 'reset', + 'setAnonymousId', + 'startSession', + 'endSession', + 'consent', + 'addCustomIntegration', + ]; + + methodNames.forEach((methodName) => { + // using Object.assign gets around the types being "wrong". + Object.assign(preloadRudderstack, { + [methodName]: function () { + preloadRudderstack.push([methodName].concat(Array.prototype.slice.call(arguments))); + }, + }); + }); + + Object.assign(window, { rudderanalytics: preloadRudderstack }); + + window.rudderanalytics?.load?.(options.writeKey, options.dataPlaneUrl, { + configUrl: options.configUrl, + destSDKBaseURL: options.integrationsUrl, + // these storage settings allow backwards compat across subdomains with the older rudderstack + // version. This would have to be changed to the new setting across all instances at once, + // and wouldn't be able to be rolled back. + storage: { + encryption: { + version: 'legacy', + }, + migrate: false, + }, + }); + + if (options.user) { + const { identifier, intercomIdentifier } = options.user.analytics; + const apiOptions: RudderstackAPIOptions = {}; + + if (intercomIdentifier) { + apiOptions.Intercom = { + user_hash: intercomIdentifier, + }; + } + + window.rudderanalytics?.identify?.( + identifier, + { + email: options.user.email, + orgId: options.user.orgId, + language: options.user.language, + version: options.buildInfo.version, + edition: options.buildInfo.edition, + }, + apiOptions + ); + } + + window.rudderanalytics?.page?.(); + } + + addEvent = (e: PageviewEchoEvent) => { + if (!window.rudderanalytics) { + return; + } + + if (isPageviewEvent(e)) { + window.rudderanalytics.page?.(); + } + + if (isInteractionEvent(e)) { + window.rudderanalytics.track?.(e.payload.interactionName, e.payload.properties); + } + + if (isExperimentViewEvent(e)) { + window.rudderanalytics.track?.('experiment_viewed', { + experiment_id: e.payload.experimentId, + experiment_group: e.payload.experimentGroup, + experiment_variant: e.payload.experimentVariant, + }); + } + }; + + // Not using Echo buffering, addEvent above sends events to GA as soon as they appear + flush = () => {}; +} diff --git a/public/app/core/services/echo/init.ts b/public/app/core/services/echo/init.ts index 3300b4c6bef..d4ff585b700 100644 --- a/public/app/core/services/echo/init.ts +++ b/public/app/core/services/echo/init.ts @@ -146,7 +146,11 @@ async function initRudderstackBackend() { return; } - const { RudderstackBackend } = await import('./backends/analytics/RudderstackBackend'); + const modulePromise = config.featureToggles.rudderstackUpgrade + ? import('./backends/analytics/RudderstackV3Backend') + : import('./backends/analytics/RudderstackBackend'); + + const { RudderstackBackend } = await modulePromise; registerEchoBackend( new RudderstackBackend({ writeKey: config.rudderstackWriteKey,