Less magical custom hash mechanism.

Signed-off-by: Eric Peterson <i.am@eric.pe>
This commit is contained in:
Eric Peterson
2022-01-30 18:51:18 +01:00
parent 35d1d675b5
commit 919a77845d
5 changed files with 45 additions and 35 deletions
+19 -22
View File
@@ -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;
}
-1
View File
@@ -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",
@@ -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 () => {
@@ -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);