Skip to content

Commit 1b16e2e

Browse files
committed
Update README, crypto Implementation, and jsdoc comments
1 parent b6df071 commit 1b16e2e

File tree

4 files changed

+105
-82
lines changed

4 files changed

+105
-82
lines changed

README.md

Lines changed: 62 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
# Telemetry Deck JavaScript SDK
22

3-
This package allows you to send signals to [TelemetryDeck](https://telemetrydeck.com) from your JavaScript code.
3+
This package allows you to send signals to [TelemetryDeck](https://telemetrydeck.com) from JavaScript code.
44

5-
It has no dependencies and supports **modern evergreen browsers** and modern versions of Node.js with support for [cryptography](https://caniuse.com/cryptography).
5+
TelemetryDeck allows you to capture and analyize users moving through your app and get help deciding how to grow, all without compromising privacy!
66

7-
Signals sent with this SDK do not send any default values, besides signal `type`, `appID`, `user` and `sessionID`.
7+
> [!NOTE]
8+
> If you want to use TelemetryDeck for your blog or static website, we recommend the [TelemetryDeck Web SDK](https://github.com/TelemetryDeck/WebSDK) instead of this JavaScript SDK.
89
9-
If you want to use this package in your web application, see recommended parameters below.
10+
# Set Up
1011

11-
## Usage
12+
The TelemetryDeck SDK has no dependencies and supports **modern evergreen browsers** and **modern versions of Node.js** with support for [cryptography](https://caniuse.com/cryptography).
1213

13-
### 📦 Advanced usage for applications that use a bundler (like Webpack, Rollup, …)
14+
## Set up in Browser Based Applications that use a bundler (React, Vue, Angular, Svelte, Ember, …)
1415

15-
After installing the package via NPM, use it like this:
16+
### 1. Installing the package
17+
18+
Please install the package using npm or the package manager of your choice
19+
20+
### 2. Initializing TelemetryDeck
21+
22+
Initialize the TelemetryDeck SDK with your app ID and your user's user identifer.
1623

1724
```javascript
1825
import TelemetryDeck from '@telemetrydeck/sdk';
@@ -21,88 +28,85 @@ const td = new TelemetryDeck({
2128
appID: '<YOUR_APP_ID>'
2229
user: '<YOUR_USER_IDENTIFIER>',
2330
});
31+
```
2432

25-
// Basic signal
26-
td.signal('<SIGNAL_TYPE>');
33+
Please replace `<YOUR_APP_ID>` with the app ID in TelemetryDeck ([Dashboard](https://dashboard.telemetrydeck.com) -> App -> Set Up App).
2734

28-
// Adanced: Signal with custom payload
29-
td.signal('<SIGNAL_TYPE>', {
30-
volume: '11',
31-
});
32-
```
35+
You also need to identify your logged in user. Instead of `<YOUR_USER_IDENTIFIER>`, pass in any string that uniquely identifies your user, such as an email address. It will be cryptographically anonymized with a hash function.
3336

34-
Please replace `YOUR_APP_ID` with the app ID you received from TelemetryDeck. If you have any string that identifies your user, such as an email address, use it as `YOUR_USER_IDENTIFIER`it will be cryptographically anonymized with a hash function.
37+
If can't specify a user identifer at initialization, you can set it later by setting `td.clientUser`.
3538

36-
If you want to pass optional parameters to the signal being sent, add them to the optional payload object.
39+
Please note that `td.signal` is an async function that returns a promise.
3740

38-
## Usage with React Native
41+
## Set up in Node.js Applications
3942

40-
React Native does not support the `crypto` module, which is required for the SDK to work. We found [react-native-quick-crypto](https://github.com/margelo/react-native-quick-crypto) to be a suitable polyfill. Please note that this is not an officially supported solution.
43+
### 1. Installing the package
4144

42-
## Queueing Signals
45+
Please install the package using npm or the package manager of your choice
4346

44-
The `TelemetryDeck` class comes with a built-in queuing mechanism for storing signals until they are flushed in a single request. Queued signals are sent with `receivedAt` prefilled with the time they were queued.
47+
### 2. Initializing TelemetryDeck
4548

46-
This uses an in-memory store by default. The store is not persisted between page reloads or app restarts. If you want to persist the store, you can pass a `store` object to the `TelemetryDeck` constructor. The store must implement the following interface:
49+
Initialize the TelemetryDeck SDK with your app ID and your user's user identifer. Since `globalThis.crypto.subtle.digest` does not exist in Node.js, you need to pass in an alternative implementation provided by Node.js.
4750

4851
```javascript
49-
export class Store {
50-
async push() // signal bodys are async and need to be awaited before stored
51-
clear() // called after flush
52-
values() // returns an array of resolved signal bodys in the order they were pushed
53-
}
54-
```
52+
import TelemetryDeck from '@telemetrydeck/sdk';
53+
import crypto from 'crypto';
5554

56-
The default implementation can be found in `src/utils/store.js` and uses a monotone counter to keep track of the order of signals.
55+
const td = new TelemetryDeck({
56+
appID: '<YOUR_APP_ID>'
57+
user: '<YOUR_USER_IDENTIFIER>',
58+
cryptoDigest: crypto.webcrypto.subtle.digest,
59+
});
60+
```
5761

58-
### 📱 You need an App ID
62+
Please replace `<YOUR_APP_ID>` with the app ID in TelemetryDeck ([Dashboard](https://dashboard.telemetrydeck.com) -> App -> Set Up App).
5963

60-
Every application and website registered to TelemetryDeck has a unique ID that we use to assign incoming signals to the correct app. To get started, create a new app in the TelemetryDeck UI and copy the ID from there.
64+
You also need to identify your logged in user. Instead of `<YOUR_USER_IDENTIFIER>`, pass in any string that uniquely identifies your user, such as an email address. It will be cryptographically anonymized with a hash function.
6165

62-
### 👤 Optional: User Identifiers
66+
If can't specify a user identifer at initialization, you can set it later by setting `td.clientUser`.
6367

64-
TelemetryDeck can count users if you assign it a unique identifier for each user that doesn't change. This identifier can be any string that is unique to the user, such as their email address, or a randomly generated UUID.
68+
Please note that `td.signal` is an async function that returns a promise.
6569

66-
Feel free to use personally identifiable information as the user identifier: We use a cryptographically secure double-hashing process on client and server to make sure the data that arrives at our servers is anonymized and can not be traced back to individual users via their identifiers. A user's identifier is hashed inside the library and then salted+hashed again on arrival at the server. This way the data is anonymized as defined by the GDPR and you don't have to ask for user consent for processing or storing this data.
70+
> [!NOTE]
71+
> If you are using React Native, React Native does not support the `crypto` module, which is required for the SDK to work. We found [react-native-quick-crypto](https://github.com/margelo/react-native-quick-crypto) to be a suitable polyfill. Please note that this is not an officially supported solution.
6772
68-
### 🚛 Optional: Payload
73+
## Advanced Initalization Options
6974

70-
You can optionally attach an object with values to the signal. This will allow you to filter and aggregate signals by these values in the dashboard.
75+
See the [source code](./src/telemetrydeck.js#L6-L17) for a full list of availble options acepted by the `TelemetryDeck` constructor.
7176

72-
Values will be stringified through `JSON.stringify()`, with few exceptions:
77+
# Sending Signals
7378

74-
- Dates will be converted to ISO strings using `.toISOString()`
75-
- Strings are passed as is (this prevents the JSON stringification of strings, which would add quotes around the string)
76-
- `floatValue` is the only key in the payload that may hold a float value. Any value passed to this will be converted to a float using `Number.parseFloat()`.
79+
Send a basic signal by calling `td.signal()` with a signal type:
7780

78-
#### Payload Recommendations for Web Apps
81+
```javascript
82+
td.signal('<SIGNAL_TYPE>');
83+
```
7984

80-
In most web apps you probably want to see a few default values which you can read from the browser. We recommend sending the following values in your custom payload:
85+
Send a signal with a custom payload by passing an object as the second argument. The payload's values will be [converted to Strings](./src/tests/store.test.js.js#L278-L310), except for `floatValue`, which can be a Float.
8186

8287
```javascript
83-
td.signal('navigation', {
84-
referrer: globalThis.document?.referrer,
85-
locale: globalThis.navigator?.language,
86-
url: globalThis.location?.href,
87-
// ...
88+
td.signal('Volume.Set', {
89+
band: 'Spinal Tap',
90+
floatValue: 11.0,
8891
});
8992
```
9093

91-
#### Test Mode
94+
# Advanced: Queueing Signals
95+
96+
The `TelemetryDeck` class comes with a built-in queuing mechanism for storing signals until they are flushed in a single request. Queued signals are sent with `receivedAt` prefilled with the time they were queued.
9297

93-
You can enable test mode by setting `testMode` to `true` on your `TelemetryDeck` instance.
98+
This uses an in-memory store by default. The store is not persisted between page reloads or app restarts. If you want to persist the store, you can pass a `store` object to the `TelemetryDeck` constructor. The store must implement the following interface:
9499

95100
```javascript
96-
td.testMode = true;
97-
td.signal('navigation', {
98-
/* ... */
99-
}); // send with testMode enabled
100-
td.testMode = false;
101-
td.signal('navigation', {
102-
/* ... */
103-
}); // send with testMode disabled
101+
export class Store {
102+
async push() // signal bodys are async and need to be awaited before stored
103+
clear() // called after flush
104+
values() // returns an array of resolved signal bodys in the order they were pushed
105+
}
104106
```
105107

106-
### 📚 Full Docs
108+
The default implementation can be found in `src/utils/store.js`.
109+
110+
---
107111

108-
Go to [telemetrydeck.com/docs](https://telemetrydeck.com/docs) to see all documentation articles
112+
[TelemetryDeck](https://telemetrydeck.com?source=github) helps you build better products with live usage data. Try it out for free.

src/telemetrydeck.js

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import { version } from './utils/version.js';
99
* @property {string} appID the app ID to send telemetry data to
1010
* @property {string} clientUser the clientUser ID to send telemetry data to
1111
* @property {string} [target] the target URL to send telemetry data to
12-
* @property {string} [sessionID]
13-
* @property {string} [salt]
14-
* @property {boolean} [testMode]
15-
* @property {Store} [store]
12+
* @property {string} [sessionID] An optional session ID to include in each signal
13+
* @property {string} [salt] A salt to use when hashing the clientUser ID
14+
* @property {boolean} [testMode] If "true", signals will be marked as test signals and only show up in Test Mode in the Dashbaord
15+
* @property {Store} [store] A store to use for queueing signals
16+
* @property {Function} [cryptoDigest] A function to use for calculating the SHA-256 hash of the clientUser ID. Null to use the browser's built-in crypto.subtle.digest function.
1617
*/
1718

1819
export default class TelemetryDeck {
@@ -27,7 +28,7 @@ export default class TelemetryDeck {
2728
* @param {TelemetryDeckOptions} options
2829
*/
2930
constructor(options = {}) {
30-
const { target, appID, clientUser, sessionID, salt, testMode, store } = options;
31+
const { target, appID, clientUser, sessionID, salt, testMode, store, cryptoDigest } = options;
3132

3233
if (!appID) {
3334
throw new Error('appID is required');
@@ -40,9 +41,11 @@ export default class TelemetryDeck {
4041
this.sessionID = sessionID ?? randomString();
4142
this.salt = salt;
4243
this.testMode = testMode ?? this.testMode;
44+
this.cryptoDigest = cryptoDigest;
4345
}
4446

4547
/**
48+
* Send a TelemetryDeck signal
4649
*
4750
* @param {string} type the type of telemetry data to send
4851
* @param {TelemetryDeckPayload} [payload] custom payload to be stored with each signal
@@ -56,6 +59,9 @@ export default class TelemetryDeck {
5659
}
5760

5861
/**
62+
* Enqueue a signal to be sent to TelemetryDeck later.
63+
*
64+
* Use flush() to send all queued signals.
5965
*
6066
* @param {string} type
6167
* @param {TelemetryDeckPayload} [payload]
@@ -70,6 +76,9 @@ export default class TelemetryDeck {
7076
}
7177

7278
/**
79+
* Send all queued signals to TelemetryDeck.
80+
*
81+
* Enqueue signals with queue().
7382
*
7483
* @returns <Promise<Response>> a promise with the response from the server, echoing the sent data
7584
*/
@@ -81,8 +90,23 @@ export default class TelemetryDeck {
8190
return flushPromise;
8291
}
8392

93+
_clientUser = '';
94+
_clientUserHashed = '';
95+
96+
async _hashedClientUser(clientUser) {
97+
if (clientUser !== this._clientUser) {
98+
this._clientUserHashed = await sha256(
99+
[this.clientUser, this.salt].join(''),
100+
this.cryptoDigest
101+
);
102+
this._clientUser = this.clientUser;
103+
}
104+
105+
return this._clientUserHashed;
106+
}
107+
84108
async _build(type, payload, options, receivedAt) {
85-
const { appID, salt, testMode } = this;
109+
const { appID, testMode } = this;
86110
let { clientUser, sessionID } = this;
87111

88112
options = options ?? {};
@@ -101,7 +125,7 @@ export default class TelemetryDeck {
101125
throw new Error(`TelemetryDeck: "clientUser" is not set`);
102126
}
103127

104-
clientUser = await sha256([clientUser, salt].join(''));
128+
clientUser = await this._hashedClientUser(clientUser);
105129

106130
const body = {
107131
clientUser,

src/utils/crypto.js

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/utils/sha256.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
import { getCrypto } from './crypto.js';
2-
3-
// https://stackoverflow.com/a/48161723/54547
4-
export async function sha256(message) {
5-
const crypto = await getCrypto();
6-
1+
/**
2+
* Calculate the SHA-256 hash of a string using a provided crypto digest function.
3+
* Defaults to globalThis.crypto.subtle.digest if available.
4+
*
5+
* // https://stackoverflow.com/a/48161723/54547
6+
*
7+
* @param {Function} cryptoDigest
8+
* @param {string} message
9+
* @returns {Promise<string>}
10+
*/
11+
export async function sha256(message, cryptoDigest = globalThis?.crypto?.subtle?.digest) {
712
// encode as UTF-8
813
const messageBuffer = new TextEncoder().encode(message);
914

1015
// hash the message
11-
const hashBuffer = await crypto.subtle.digest('SHA-256', messageBuffer);
16+
const hashBuffer = await cryptoDigest('SHA-256', messageBuffer);
1217

1318
// convert ArrayBuffer to Array
1419
const hashArray = [...new Uint8Array(hashBuffer)];

0 commit comments

Comments
 (0)