Less magical custom hash mechanism.
Signed-off-by: Eric Peterson <i.am@eric.pe>
This commit is contained in:
@@ -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<string> => {
|
||||
return customHashingFunction(userEntityRef);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
class PseudononymizedIdentity implements IdentityApi {
|
||||
constructor(private actualApi: IdentityApi) {}
|
||||
async getBackstageIdentity(): Promise<BackstageUserIdentity> {
|
||||
const { email = 'someone' } = await this.actualApi.getProfileInfo();
|
||||
const hashedEmail = customHashingFunction(email);
|
||||
|
||||
return {
|
||||
type: 'user',
|
||||
userEntityRef: `PrivateUser:default/${hashedEmail}`,
|
||||
ownershipEntityRefs: [],
|
||||
};
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Debugging and Testing
|
||||
|
||||
@@ -19,6 +19,9 @@ export class GoogleAnalytics implements AnalyticsApi {
|
||||
config: Config,
|
||||
options?: {
|
||||
identityApi?: IdentityApi;
|
||||
userIdTransform?:
|
||||
| 'sha-256'
|
||||
| ((userEntityRef: string) => Promise<string>);
|
||||
},
|
||||
): GoogleAnalytics;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
+7
-5
@@ -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 () => {
|
||||
|
||||
+16
-7
@@ -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<string>;
|
||||
private readonly capture: DeferredCapture;
|
||||
|
||||
/**
|
||||
@@ -46,6 +46,7 @@ export class GoogleAnalytics implements AnalyticsApi {
|
||||
*/
|
||||
private constructor(options: {
|
||||
identityApi?: IdentityApi;
|
||||
userIdTransform?: 'sha-256' | ((userEntityRef: string) => Promise<string>);
|
||||
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<string>);
|
||||
} = {},
|
||||
) {
|
||||
// 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<string> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user