diff --git a/plugins/analytics-module-ga/README.md b/plugins/analytics-module-ga/README.md index 1a4bf13aad..92cfd7e9ae 100644 --- a/plugins/analytics-module-ga/README.md +++ b/plugins/analytics-module-ga/README.md @@ -128,35 +128,32 @@ enable this... Note that, to comply with GA policies, the value of the User ID is pseudonymized before being sent to GA. By default, it is a `sha256` hash of the current user's `userEntityRef` as returned by the `identityApi`. To set a -different value, provide a custom implementation of the `identityApi` that -resolves a `userEntityRef` of the form `PrivateUser:namespace/YOUR-VALUE`. For -example: +different value, provide a `userIdTransform` function alongside `identityApi` +when you instantiate `GoogleAnalytics`. This function will be passed the +`userEntityRef` as an argument and should resolve to the value you wish to set +as the user ID. For example: ```typescript +import { + analyticsApiRef, + configApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { GoogleAnalytics } from '@backstage/plugin-analytics-module-ga'; + export const apis: AnyApiFactory[] = [ createApiFactory({ api: analyticsApiRef, - deps: { config: configApiRef, identityApi: identityApiRef }, - factory: ({ identityApi, config }) => { - return new PseudononymizedIdentity(identityApi); - }, + deps: { configApi: configApiRef, identityApi: identityApiRef }, + factory: ({ configApi, identityApi }) => + GoogleAnalytics.fromConfig(configApi, { + identityApi, + userIdTransform: async (userEntityRef: string): Promise => { + return customHashingFunction(userEntityRef); + }, + }), }), ]; - -class PseudononymizedIdentity implements IdentityApi { - constructor(private actualApi: IdentityApi) {} - async getBackstageIdentity(): Promise { - const { email = 'someone' } = await this.actualApi.getProfileInfo(); - const hashedEmail = customHashingFunction(email); - - return { - type: 'user', - userEntityRef: `PrivateUser:default/${hashedEmail}`, - ownershipEntityRefs: [], - }; - } - // ... -} ``` ### Debugging and Testing diff --git a/plugins/analytics-module-ga/api-report.md b/plugins/analytics-module-ga/api-report.md index a396083444..45bb84a421 100644 --- a/plugins/analytics-module-ga/api-report.md +++ b/plugins/analytics-module-ga/api-report.md @@ -19,6 +19,9 @@ export class GoogleAnalytics implements AnalyticsApi { config: Config, options?: { identityApi?: IdentityApi; + userIdTransform?: + | 'sha-256' + | ((userEntityRef: string) => Promise); }, ): GoogleAnalytics; } diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 264c46410f..255a0cae8e 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -21,7 +21,6 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/core-components": "^0.8.6", "@backstage/core-plugin-api": "^0.6.0", diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts index 57a4d4f4c4..a4f7195fec 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts @@ -270,10 +270,8 @@ describe('GoogleAnalytics', () => { }); }); - it('sets pre-hashed userId when PrivateUser entity ref is provided', async () => { - (identityApi.getBackstageIdentity as jest.Mock).mockResolvedValueOnce({ - userEntityRef: 'PrivateUser:hashed/s0m3hash3dvalu3', - }); + it('set custom-hashed userId when userIdTransform is provided', async () => { + const userIdTransform = jest.fn().mockResolvedValue('s0m3hash3dvalu3'); const optionalConfig = new ConfigReader({ app: { analytics: { @@ -281,7 +279,10 @@ describe('GoogleAnalytics', () => { }, }, }); - const api = GoogleAnalytics.fromConfig(optionalConfig, { identityApi }); + const api = GoogleAnalytics.fromConfig(optionalConfig, { + identityApi, + userIdTransform, + }); api.captureEvent({ action: 'navigate', subject: '/', @@ -297,6 +298,7 @@ describe('GoogleAnalytics', () => { expect(setData).toMatchObject({ userId: 's0m3hash3dvalu3', }); + expect(userIdTransform).toHaveBeenCalledWith('User:default/someone'); }); it('does not set userId when identityApi is provided and ga.identity is explicitly disabled', async () => { diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts index 6e5a12df4a..bdbdaebee7 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts @@ -15,7 +15,6 @@ */ import ReactGA from 'react-ga'; -import { parseEntityRef } from '@backstage/catalog-model'; import { AnalyticsApi, AnalyticsContextValue, @@ -39,6 +38,7 @@ type CustomDimensionOrMetricConfig = { */ export class GoogleAnalytics implements AnalyticsApi { private readonly cdmConfig: CustomDimensionOrMetricConfig[]; + private customUserIdTransform?: (userEntityRef: string) => Promise; private readonly capture: DeferredCapture; /** @@ -46,6 +46,7 @@ export class GoogleAnalytics implements AnalyticsApi { */ private constructor(options: { identityApi?: IdentityApi; + userIdTransform?: 'sha-256' | ((userEntityRef: string) => Promise); cdmConfig: CustomDimensionOrMetricConfig[]; identity: string; trackingId: string; @@ -58,6 +59,7 @@ export class GoogleAnalytics implements AnalyticsApi { identity, trackingId, identityApi, + userIdTransform = 'sha-256', scriptSrc, testMode, debug, @@ -76,6 +78,10 @@ export class GoogleAnalytics implements AnalyticsApi { // If identity is required, defer event capture until identity is known. this.capture = new DeferredCapture({ defer: identity === 'required' }); + // Allow custom userId transformation. + this.customUserIdTransform = + typeof userIdTransform === 'function' ? userIdTransform : undefined; + // Capture user only when explicitly enabled and provided. if (identity !== 'disabled' && identityApi) { this.setUserFrom(identityApi); @@ -87,7 +93,12 @@ export class GoogleAnalytics implements AnalyticsApi { */ static fromConfig( config: Config, - options: { identityApi?: IdentityApi } = {}, + options: { + identityApi?: IdentityApi; + userIdTransform?: + | 'sha-256' + | ((userEntityRef: string) => Promise); + } = {}, ) { // Get all necessary configuration. const trackingId = config.getString('app.analytics.ga.trackingId'); @@ -220,11 +231,9 @@ export class GoogleAnalytics implements AnalyticsApi { * Returns a PII-free user ID for use in Google Analytics. */ private getPrivateUserId(userEntityRef: string): Promise { - const entity = parseEntityRef(userEntityRef); - - // Mechanism allowing integrators to provide their own hashed values. - if (entity.kind === 'PrivateUser') { - return Promise.resolve(entity.name); + // Allow integrators to provide their own hashing transformer. + if (this.customUserIdTransform) { + return this.customUserIdTransform(userEntityRef); } return this.hash(userEntityRef);