Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEV2-5321 Add RudderStack GitBook Integration #1

Merged
merged 7 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module.exports = {
env: {
es2021: true,
node: true
},
extends: [
'airbnb-typescript/base',
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:import/errors',
'plugin:import/typescript',
'prettier'
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname
},
plugins: ['@typescript-eslint', 'import'],
rules: {
'no-void': 'off',
'no-console': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/camelcase': 'off',
'class-methods-use-this': 'off',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'**/*.test.[t|j]s',
'**/*.spec.[t|j]s',
'./src/app/tests/**/*.[t|j]s'
]
}
]
}
};
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# The following github team will be considered code-owners for all account repositories
# code-owners will be later applied as merge approvers for all pull requests in order to comply with our SOC2 Policy.
* @codota/merge-approvers
39 changes: 39 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: 'CI'

on:
push:
branches: ['main']
pull_request:

jobs:
build:
name: 'Build'
runs-on: ubuntu-latest
steps:
- name: Checkout project
uses: actions/checkout@v2

- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: '18.16'

- uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: .yarn/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Install
run: yarn --immutable

- name: Prettier
run: yarn prettier:check

- name: Lint
run: yarn lint

- name: Test
run: yarn test
21 changes: 21 additions & 0 deletions .github/workflows/enforce-pr-jira-association.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Enforce PR-Jira association

on:
pull_request:
types:
- opened
- reopened
- edited
- synchronize

jobs:
enforce-issue:
runs-on: ubuntu-latest
name: JIRA Association
steps:
- name: Check for JIRA ISSUE
id: check
uses: usehaystack/jira-pr-link-action@v4
with:
ignore-author: dependabot[bot]
project: 'DEV2'
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Release

on: workflow_dispatch

jobs:
publish:
name: Publish to GitBook
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x
- run: npm ci
- run: npm install -g @gitbook/cli
- run: gitbook publish .
env:
GITBOOK_TOKEN: ${{ secrets.GITBOOK_TOKEN }}
1 change: 1 addition & 0 deletions .husky/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_
5 changes: 5 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn prettier
yarn lint:fix
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v18.16.0
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# @gitbook/integration-segment

## 1.0.0

### Major Changes

### Minor Changes

### Patch Changes
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# gitbook-integration-rudderstack

A Gitbook plugin for sending events to RudderStack
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { presets: ['@babel/preset-env'] };
44 changes: 44 additions & 0 deletions gitbook-manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: rudderstack
title: RudderStack
icon: ./assets/icon.png
organization: tabnine
description: Integrate your GitBook public documentation with your RudderStack pipeline.
visibility: private
script: ./src/index.ts
scopes:
- space:views:read
summary: |
# Overview

The GitBook Segment integration empowers your teams by connecting your GitBook public documentation to your Segment pipeline.

# How it works

Automatic page view events on your public documentation:
Each of your connected GitBook spaces will send a `GitBook Space Viewed` event to the RudderStack source associated via id.

Track your users' actions across all your company's websites:
- Each `GitBook Space Viewed` includes the `anonymousId` property extracted from the RudderStack cookie.
- If the active user has already visited one of your other company's websites, the `anonymousId` will match the one already set by RudderStack.
- Otherwise, GitBook will associate a new `anonymousId` for the user matching RudderStack's format.

# Configure

To install the RudderStack integration on a single space click on the Integrations button in sub-navigation. Alternatively, you can install it on multiple or all spaces by going into your organization settings.
You will need the RudderStack Source Write Key to help you identify the data source, and the Data Plane URL for your RudderStack dataplane.
categories:
- analytics
configurations:
space:
properties:
data_plane_url:
type: string
title: Data Plane URL
description: The URL address of the RudderStack Data Plane.
source_write_key:
type: string
title: Write Key
description: The write key associated with the event source.
required:
- data_plane_url
- source_write_key
7 changes: 7 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest'
}
};
53 changes: 53 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "integration-rudderstack",
"version": "1.0.2",
"private": true,
"dependencies": {
"@gitbook/api": "*",
"@gitbook/runtime": "*",
"crypto-js": "^4.2.0"
},
"devDependencies": {
"@babel/preset-env": "^7.23.9",
"@gitbook/cli": "*",
"@gitbook/eslint-config": "*",
"@gitbook/tsconfig": "*",
"@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.12",
"@types/node": "^18.16.0",
"assert": "^2.0.0",
"babel-jest": "^29.7.0",
"esbuild-register": "^3.5.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "7",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "2",
"eslint-plugin-json": "^3.1.0",
"husky": "^9.0.11",
"jest": "^29.7.0",
"test": "^3.2.1",
"ts-jest": "^29.1.2",
"typescript": "^5.3.3"
},
"scripts": {
"typecheck": "tsc --noEmit",
"publish-integration": "gitbook publish .",
"test": "jest --passWithNoTests",
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint --fix .",
"prepare": "husky install",
"prettier": "prettier --write .",
"prettier:check": "prettier --check ."
},
"prettier": {
"useTabs": false,
"trailingComma": "none",
"singleQuote": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid"
},
"eslintIgnore": [
"*.test.ts",
".eslintrc.js"
]
}
81 changes: 81 additions & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as api from '@gitbook/api';
import { AES, enc } from 'crypto-js';

import { version } from '../package.json';

const RUDDER_ANONYMOUS_ID_COOKIE_NAME = 'rl_anonymous_id';

/**
* Generate the event to track in RudderStack for an actual GitBook event.
*/
export default function generateRudderStackTrackEvent(
event: api.SpaceViewEvent
) {
const { visitor, referrer, url, spaceId, pageId } = event;

const anonymousId = getAnonymousId(event);
const visitedURL = new URL(url);
return {
event: 'GitBook Space Viewed',
anonymousId,
context: {
library: {
name: 'GitBook',
version
},
page: {
path: visitedURL.pathname,
search: visitedURL.search,
url,
referrer
},
userAgent: visitor.userAgent,
ip: visitor.ip
},
properties: {
spaceId,
pageId
}
};
}

/**
* Return the anonymous ID we send to RudderStack in the Track event.
*
* Retrieve the value from the `rl_anonymous_id` RudderStack cookie if present.
* This allows to consolidate the track event with other events generated by an anonymous user
* that already has visited the customer website (where RudderStack tracking is setup).
*
* When there is no `rl_anonymous_id` cookie, we fallback to using the GitBook anonymous ID.
*/
function getAnonymousId(event: api.SpaceViewEvent): string {
const { visitor } = event;
const { cookies } = visitor;

const extractedAnonymousId = extractAnonymousId(cookies);
console.log('extracted anonymous id', extractedAnonymousId);
return extractedAnonymousId || visitor.anonymousId;
}

function decrypt(urlDecodedCookieValue: string): string {
return AES.decrypt(
urlDecodedCookieValue.substring('RudderEncrypt:'.length),
'Rudder'
).toString(enc.Utf8);
}

function extractAnonymousId(cookies: { [p: string]: string }): string {
let anonymousId: string;

if (cookies) {
const rudderAnonymousId = cookies[RUDDER_ANONYMOUS_ID_COOKIE_NAME];
if (rudderAnonymousId) {
const decodedAnonymousId = decodeURIComponent(rudderAnonymousId);
const decryptedId = decrypt(decodedAnonymousId);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
anonymousId = JSON.parse(decryptedId);
}
}

return anonymousId;
}
50 changes: 50 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
createIntegration,
RuntimeContext,
RuntimeEnvironment
} from '@gitbook/runtime';

import generateRudderStackTrackEvent from './events';

type RudderStackRuntimeContext = RuntimeContext<
RuntimeEnvironment<
// eslint-disable-next-line @typescript-eslint/ban-types
{},
{
data_plane_url?: string;
source_write_key?: string;
}
>
>;

export default createIntegration<RudderStackRuntimeContext>({
events: {
space_view: async (event, { environment }) => {
const writeKey =
environment.spaceInstallation?.configuration.source_write_key;
if (!writeKey) {
throw new Error(
`The RudderStack source write key is missing from the Space (ID: ${event.spaceId}) installation.`
);
}

const dataPlaneUrl =
environment.spaceInstallation?.configuration.data_plane_url;
if (!dataPlaneUrl) {
throw new Error(
`The RudderStack data plane URL is missing from the Space (ID: ${event.spaceId}) installation.`
);
}

const trackEvent = generateRudderStackTrackEvent(event);
await fetch(`${dataPlaneUrl}/v1/track`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${writeKey}:`)}`
},
body: JSON.stringify(trackEvent)
});
}
}
});
Loading
Loading