Skip to content

Commit

Permalink
feat(analytics-module-segment): create the analytics plugin module fo…
Browse files Browse the repository at this point in the history
…r segment (#12)

Signed-off-by: Alec Jacobs <[email protected]>
  • Loading branch information
alecjacobs5401 authored Sep 6, 2024
1 parent df9e8a7 commit 797ab8d
Show file tree
Hide file tree
Showing 18 changed files with 1,225 additions and 6 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@

<br>

This repository contains a collection of [Backstage](https://backstage.io) plugins created and maintained by [Twilio Segment](https://segment.com). Installation instructions for each plugin can be found in their respective READMEs.
This repository contains a collection of [Backstage](https://backstage.io) plugins created and maintained by [Twilio Segment][segment]. Installation instructions for each plugin can be found in their respective READMEs.

| Package | Description |
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| [@segment/backstage-plugin-proxy-sigv4-backend](./plugins/proxy-sigv4-backend) | A Backstage backend plugin that proxies requests to AWS services using SigV4 authentication. |
| Package | Description |
| ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| [@segment/backstage-plugin-analytics-module-segment](./plugins/analytics-module-segment) | A Backstage frontend analytics module plugin that provides [Backstage Analytics][analytics] to [Segment][segment] |
| [@segment/backstage-plugin-proxy-sigv4-backend](./plugins/proxy-sigv4-backend) | A Backstage backend plugin that proxies requests to AWS services using SigV4 authentication. |

<br>

Expand All @@ -34,3 +35,6 @@ This repository contains a collection of [Backstage](https://backstage.io) plugi
## License

Copyright 2023 Twilio Inc. Licensed under the Apache License, Version 2.0: <https://www.apache.org/licenses/LICENSE-2.0>

[analytics]: https://backstage.io/docs/plugins/analytics
[segment]: https://segment.com
6 changes: 6 additions & 0 deletions app-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
app:
title: Scaffolded Backstage App
baseUrl: http://localhost:3000
analytics:
segment:
enabled: true
writeKey: ${SEGMENT_WRITE_KEY:-foobar}
testMode: true
debug: true

organization:
name: My Company
Expand Down
1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@backstage/theme": "^0.5.6",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@segment/backstage-plugin-analytics-module-segment": "link:../../plugins/analytics-module-segment",
"history": "^5.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import {
ScmAuth,
} from '@backstage/integration-react';
import {
analyticsApiRef,
AnyApiFactory,
configApiRef,
createApiFactory,
identityApiRef,
} from '@backstage/core-plugin-api';
import { SegmentAnalytics } from '@segment/backstage-plugin-analytics-module-segment';

export const apis: AnyApiFactory[] = [
createApiFactory({
Expand All @@ -16,4 +19,10 @@ export const apis: AnyApiFactory[] = [
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
ScmAuth.createDefaultApiFactory(),
createApiFactory({
api: analyticsApiRef,
deps: { configApi: configApiRef, identityApi: identityApiRef },
factory: ({ configApi, identityApi }) =>
SegmentAnalytics.fromConfig(configApi, { identityApi }),
}),
];
1 change: 1 addition & 0 deletions plugins/analytics-module-segment/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
210 changes: 210 additions & 0 deletions plugins/analytics-module-segment/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Analytics Module: Segment

This plugin provides an opinionated implementation of the Backstage Analytics
API for [Segment][segment]. Once installed and configured, analytics events will
be sent to the configured Segment Workspace as your users navigate and use your Backstage instance.

## Requirements

This plugin requires an active workspace with [Segment][segment]. Please reference the [Getting Started Guide][getting-started] to get set up before proceeding.

## Installation

### Install the plugin package in your Backstage app:

```sh
# From your Backstage root directory
yarn --cwd packages/app add @segment/backstage-plugin-analytics-module-segment
```

### Wire up the API implementation to your App:

```ts
// packages/app/src/apis.ts
import {
analyticsApiRef,
configApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { SegmentAnalytics } from '@segment/backstage-plugin-analytics-module-segment';

export const apis: AnyApiFactory[] = [
// Instantiate and register the SegmentAnalytics API Implementation.
createApiFactory({
api: analyticsApiRef,
deps: { configApi: configApiRef, identityApi: identityApiRef },
factory: ({ configApi, identityApi }) =>
SegmentAnalytics.fromConfig(configApi, {
identityApi,
}),
}),
];
```

#### Optional: Configure a user transform

By default, this analytics plugin [identifies][identify] the user taking actions as the logged in Backstage User's entity reference string (e.g. `user:development/guest`). Currently, no other information is provided to the identify call.

To anonymize users, a `userIdTransform` can be provided in one of two ways:

1. The string value `sha-256`
2. A custom transformation function that matches the contract of `(userEntityRef: string) => Promise<string>`

If `sha-256` is provided, the user entity reference will be pseudonymized into a sha256 string value.

```ts
// packages/app/src/apis.ts
import {
analyticsApiRef,
configApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { SegmentAnalytics } from '@segment/backstage-plugin-analytics-module-segment';

export const apis: AnyApiFactory[] = [
// Instantiate and register the SegmentAnalytics API Implementation.
createApiFactory({
api: analyticsApiRef,
deps: { configApi: configApiRef, identityApi: identityApiRef },
factory: ({ configApi, identityApi }) =>
SegmentAnalytics.fromConfig(configApi, {
identityApi,
userIdTransform: 'sha-256',
}),
}),
];
```

For enhanced security, providing a custom transformation function can be used to hash the value in any means desired.

```ts
// packages/app/src/apis.ts
import {
analyticsApiRef,
configApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { SegmentAnalytics } from '@segment/backstage-plugin-analytics-module-segment';

export const apis: AnyApiFactory[] = [
// Instantiate and register the SegmentAnalytics API Implementation.
createApiFactory({
api: analyticsApiRef,
deps: { configApi: configApiRef, identityApi: identityApiRef },
factory: ({ configApi, identityApi }) =>
SegmentAnalytics.fromConfig(configApi, {
identityApi,
async userIdTransform(userEntityRef: string) {
const salt = configApi.getString(
'custom.config.analytics.userIdSalt',
);
const textToChars = (text: string) =>
text.split('').map(c => c.charCodeAt(0));
const byteHex = (n: number) =>
('0' + Number(n).toString(16)).substring(-2);
const applySaltToChar = (code: number) =>
textToChars(salt).reduce((a, b) => a ^ b, code);

return textToChars(userEntityRef)
.map(applySaltToChar)
.map(byteHex)
.join('');
},
}),
}),
];
```

#### Optional: Prevent user identification

If you choose not to identify Backstage users in analytics events, simply neglect to provide the `identityApi` when initializing the `SegmentAnalytics` API.

```ts
// packages/app/src/apis.ts
import { analyticsApiRef, configApiRef } from '@backstage/core-plugin-api';
import { SegmentAnalytics } from '@segment/backstage-plugin-analytics-module-segment';

export const apis: AnyApiFactory[] = [
// Instantiate and register the SegmentAnalytics API Implementation.
createApiFactory({
api: analyticsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => SegmentAnalytics.fromConfig(configApi),
}),
];
```

Doing so will allow analytic events to continue to be sent to Segment, just with the the events being attributed to [anonymous user IDs][anonymous-ids].

### Configure the plugin in your `app-config.yaml`:

The following is the minimum configuration required to start sending analytics
events to Segment. The only requirement is the [write key][write-key] for the [Analytics.js Source][analytics.js-source] that was created for your Backstage instance.

```yaml
# app-config.yaml
app:
analytics:
segment:
writeKey: abcABCfooBARtestKEY
```
## Configuration
### Disabling
In some pre-production environments, it may not be prudent to load the Segment Analytics plugin at all. In those cases, you can explicitly disable analytics through app configuration:
```yaml
# app-config.yaml
app:
analytics:
segment:
enabled: false # Prevent the analytics instance from loading
writeKey: abcABCfooBARtestKEY # write key is still required in app-config
```
### Debugging and Testing
In pre-production environments, you may wish to set additional configurations
to turn off reporting to Analytics and/or print debug statements to the
console. In those cases, you can explicitly disable analytics through app configuration:
```yaml
app:
analytics:
segment:
writeKey: abcABCfooBARtestKEY
testMode: true # Prevents data being sent to Segment and logs what would have been sent instead
debug: true # Configure debug on the Analytics module and write the Backstage analytics event to the web console
```
### Analytics agent options
Additional configuration is available to configure the Analytics.js agent as follows:
```yaml
app:
analytics:
segment:
writeKey: abcABCfooBARtestKEY
agent:
# Disable storing any data on the client-side via cookies or localstorage
disableClientPersistence: true
# Disables automatically converting ISO string event properties into Dates.
disableAutoISOConversion: true
# Whether or not to capture page context early so that it is always up-to-date.
initialPageView: true
```
> [!NOTE]\
> The `testMode` and `debug` configuration fields work independently of each other.
> If `debug` is `true` and `testMode` is `false`, analytics events will still be sent to Segment
> and debug information will be written to the console still.

[segment]: https://segment.com/
[getting-started]: https://segment.com/docs/getting-started/
[write-key]: https://segment.com/docs/connections/find-writekey/
[analytics.js-source]: https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/
[identify]: https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/#identify
[anonymous-ids]: https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/identity/#anonymous-ids
72 changes: 72 additions & 0 deletions plugins/analytics-module-segment/config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export interface Config {
app: {
analytics: {
segment: {
/**
* Controls whether the Segment Analytics module is enabled.
* Defaults to true.
*
* @visibility frontend
*/
enabled?: boolean;

/**
* Segment Analytics Write Key. Reference https://segment.com/docs/connections/find-writekey/
* to find your write key
*
* @visibility frontend
*/
writeKey: string;

/**
* Whether to log analytics debug statements and events to the console. Does not prevent sending of events.
* Defaults to false.
*
* @visibility frontend
*/
debug?: boolean;

/**
* Prevents events from actually being sent and instead logged as console output when set to true.
* Defaults to false.
*
* @visibility frontend
*/
testMode?: boolean;

/**
* Configuration options for the Segment Analytics agent.
*
* @visibility frontend
*/
agent?: {
/**
* Disables storing any data on the client-side via cookies or localstorage.
* Defaults to false.
*
* @visibility frontend
*/
disableClientPersistence?: boolean;

/**
* Disables automatically converting ISO string event properties into Dates.
* ISO string to Date conversions occur right before sending events to a classic device mode integration,
* after any destination middleware have been ran.
* Defaults to false.
*
* @visibility frontend
*/
disableAutoISOConversion?: boolean;

/**
* Whether or not to capture page context early so that it is always up-to-date.
* Defaults to false.
*
* @visibility frontend
*/
initialPageView?: boolean;
};
};
};
};
}
10 changes: 10 additions & 0 deletions plugins/analytics-module-segment/dev/Playground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { Link } from '@backstage/core-components';

export const Playground = () => {
return (
<>
<Link to="#clicked">Click Here</Link>
</>
);
};
26 changes: 26 additions & 0 deletions plugins/analytics-module-segment/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import {
analyticsApiRef,
configApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { createDevApp } from '@backstage/dev-utils';

import { Playground } from './Playground';
import { SegmentAnalytics } from '../src';

createDevApp()
.registerApi({
api: analyticsApiRef,
deps: { configApi: configApiRef, identityApi: identityApiRef },
factory: ({ configApi, identityApi }) =>
SegmentAnalytics.fromConfig(configApi, {
identityApi,
}),
})
.addPage({
path: '/segment',
title: 'Segment Playground',
element: <Playground />,
})
.render();
Loading

0 comments on commit 797ab8d

Please sign in to comment.