Skip to content

Commit

Permalink
Merge branch 'tim/sentry' into 'master'
Browse files Browse the repository at this point in the history
feat: Add Sentry tags with Tanker info on outbound exceptions

See merge request TankerHQ/sdk-js!1002
  • Loading branch information
tux3 committed Jan 8, 2024
2 parents c4f56c6 + 8040eb5 commit c2e6bf4
Show file tree
Hide file tree
Showing 17 changed files with 282 additions and 22 deletions.
13 changes: 12 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@tanker/stream-cloud-storage": "0.0.1",
"@tanker/types": "0.0.1",
"@types/varint": "^6.0.0",
"@sentry/types": "7.76.0",
"tslib": "^2.3.1",
"varint": "^6.0.0"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/Network/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export class Client {
this._userId = userId;
}

get instanceId(): string {
return this._instanceId;
}

_cancelable = <R>(fun: (...args: Array<any>) => Promise<R>) => (...args: Array<any>) => {
// cancelationHandle.promise always rejects. Its returned type doesn't matter
if (this._cancelationHandle.settled) {
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/Resources/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { ResourceStore } from './ResourceStore';
import type { LocalUserManager } from '../LocalUser/Manager';
import type { GroupManager } from '../Groups/Manager';
import type { ProvisionalIdentityManager } from '../ProvisionalIdentity/Manager';
import { SentryLimiter } from '../SentryLimiter';

export type KeyResult = {
id: b64string;
Expand All @@ -23,18 +24,21 @@ export class ResourceManager {
declare _keyDecryptor: KeyDecryptor;
declare _keyLookupCoalescer: TaskCoalescer<KeyResult>;
declare _resourceStore: ResourceStore;
declare _sentry: SentryLimiter | null;

constructor(
client: Client,
resourceStore: ResourceStore,
localUserManager: LocalUserManager,
groupManager: GroupManager,
provisionalIdentityManager: ProvisionalIdentityManager,
sentry: SentryLimiter | null,
) {
this._client = client;
this._keyDecryptor = new KeyDecryptor(localUserManager, groupManager, provisionalIdentityManager);
this._keyLookupCoalescer = new TaskCoalescer();
this._resourceStore = resourceStore;
this._sentry = sentry;
}

async findKeyFromResourceId(resourceId: Uint8Array): Promise<Key | null> {
Expand All @@ -52,11 +56,28 @@ export class ResourceManager {
if (!resourceKey) {
const keyPublishBlock = await this._client.getResourceKey(resourceId);
if (!keyPublishBlock) {
this._sentry?.addBreadcrumb({
category: 'tanker_keystore',
level: 'warning',
message: `Key not found in either cache or server for ${b64resourceId}`,
});
return { id: b64resourceId, key: null };
}
this._sentry?.addBreadcrumb({
category: 'tanker_keystore',
level: 'debug',
message: `Tanker key not found in cache, but fetched from server for ${b64resourceId}`,
});

const keyPublish = getKeyPublishEntryFromBlock(keyPublishBlock);
resourceKey = await this._keyDecryptor.keyFromKeyPublish(keyPublish);
await this._resourceStore.saveResourceKey(resourceId, resourceKey);
} else {
this._sentry?.addBreadcrumb({
category: 'tanker_keystore',
level: 'debug',
message: `Tanker key found in cache for ${b64resourceId}`,
});
}

return { id: b64resourceId, key: resourceKey };
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/SentryLimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Breadcrumb, Hub } from '@sentry/types';

export const BREADCRUMB_LIMIT = 20;

export class SentryLimiter {
breadcrumbs: Array<Breadcrumb>;
sentryHub: Hub;

constructor(sentryHub: Hub) {
this.breadcrumbs = [];
this.sentryHub = sentryHub;
}

addBreadcrumb = (breadcrumb: Breadcrumb) => {
if (this.breadcrumbs.length == BREADCRUMB_LIMIT)
this.breadcrumbs.shift();

this.breadcrumbs.push({
timestamp: Math.floor(Date.now() / 1000),
...breadcrumb,
});
};

flush = () => {
for (const breadcrumb of this.breadcrumbs)
this.sentryHub.addBreadcrumb(breadcrumb, undefined);
this.breadcrumbs = [];
};
}
27 changes: 23 additions & 4 deletions packages/core/src/Session/Session.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import EventEmitter from 'events';
import type { Hub } from '@sentry/types';

import { TankerError, ExpiredVerification, InternalError, InvalidArgument, InvalidVerification, NetworkError, OperationCanceled, PreconditionFailed, TooManyAttempts } from '@tanker/errors';
import { utils } from '@tanker/crypto';

import type { DataStoreOptions } from './Storage';
import { Storage } from './Storage';
Expand All @@ -18,10 +20,13 @@ import { ResourceManager } from '../Resources/Manager';
import { DataProtector } from '../DataProtection/DataProtector';
import type { OidcNonceManager } from '../OidcNonce/Manager';
import { SessionManager } from '../TransparentSession/Manager';
import { SentryLimiter } from '../SentryLimiter';

export class Session extends EventEmitter {
_storage: Storage;
_client: Client;
_sentry: Hub | null;
_sentryLimiter: SentryLimiter | null;

_localUserManager: LocalUserManager;
_userManager: UserManager;
Expand All @@ -34,12 +39,14 @@ export class Session extends EventEmitter {

_status: Status;

constructor(userData: UserData, storage: Storage, oidcNonceManagerGetter: () => Promise<OidcNonceManager>, client: Client) {
constructor(userData: UserData, storage: Storage, oidcNonceManagerGetter: () => Promise<OidcNonceManager>, client: Client, sentry: Hub | null) {
super();

this._storage = storage;
this._client = client;
this._status = Status.STOPPED;
this._sentry = sentry;
this._sentryLimiter = sentry ? new SentryLimiter(sentry) : null;

this._localUserManager = new LocalUserManager(userData, oidcNonceManagerGetter, client, storage.keyStore);
this._localUserManager.on('error', async (e: Error) => {
Expand All @@ -62,7 +69,7 @@ export class Session extends EventEmitter {
this._userManager = new UserManager(client, this._localUserManager.localUser);
this._provisionalIdentityManager = new ProvisionalIdentityManager(client, storage.keyStore, this._localUserManager, this._userManager);
this._groupManager = new GroupManager(client, storage.groupStore, this._localUserManager.localUser, this._userManager, this._provisionalIdentityManager);
this._resourceManager = new ResourceManager(client, storage.resourceStore, this._localUserManager, this._groupManager, this._provisionalIdentityManager);
this._resourceManager = new ResourceManager(client, storage.resourceStore, this._localUserManager, this._groupManager, this._provisionalIdentityManager, this._sentryLimiter);
this._sessionManager = new SessionManager(storage.sessionStore);
this._dataProtector = new DataProtector(client, this._localUserManager.localUser, this._userManager, this._provisionalIdentityManager, this._groupManager, this._resourceManager, this._sessionManager);
this._cloudStorageManager = new CloudStorageManager(client, this._dataProtector);
Expand All @@ -72,20 +79,24 @@ export class Session extends EventEmitter {
return this._status;
}

get statusName(): string {
return Status[this.status];
}

set status(nextStatus: Status) {
if (nextStatus !== this._status) {
this._status = nextStatus;
this.emit('status_change', nextStatus);
}
}

static init = async (userData: UserData, oidcNonceManagerGetter: () => Promise<OidcNonceManager>, storeOptions: DataStoreOptions, clientOptions: ClientOptions): Promise<Session> => {
static init = async (userData: UserData, oidcNonceManagerGetter: () => Promise<OidcNonceManager>, storeOptions: DataStoreOptions, clientOptions: ClientOptions, sentry: Hub | null): Promise<Session> => {
const client = new Client(userData.trustchainId, userData.userId, clientOptions);

const storage = new Storage(storeOptions);
await storage.open(userData.userId, userData.userSecret);

return new Session(userData, storage, oidcNonceManagerGetter, client);
return new Session(userData, storage, oidcNonceManagerGetter, client, sentry);
};

start = async (): Promise<void> => {
Expand Down Expand Up @@ -123,6 +134,14 @@ export class Session extends EventEmitter {
try {
return await manager[func].call(manager, ...args);
} catch (e) {
const localUser = this._localUserManager.localUser;
this._sentry?.setTag('tanker_app_id', utils.toBase64(localUser.trustchainId));
this._sentry?.setTag('tanker_user_id', utils.toBase64(localUser.userId));
this._sentry?.setTag('tanker_device_id', utils.toBase64(localUser.deviceId));
this._sentry?.setTag('tanker_instance_id', this._client.instanceId);
this._sentry?.setTag('tanker_status', this.statusName);
this._sentryLimiter?.flush();

await this._handleUnrecoverableError(e);
throw e;
}
Expand Down
19 changes: 16 additions & 3 deletions packages/core/src/Tanker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import EventEmitter from 'events';
import type { b64string, EncryptionStream, DecryptionStream } from '@tanker/crypto';
import { randomBase64Token, ready as cryptoReady, tcrypto, utils, extractEncryptionFormat, SAFE_EXTRACTION_LENGTH, assertResourceId } from '@tanker/crypto';
import { InternalError, InvalidArgument } from '@tanker/errors';
import { globalThis } from '@tanker/global-this';
import { assertDataType, assertInteger, assertNotEmptyString, castData } from '@tanker/types';
import type { Data, ResourceMetadata } from '@tanker/types';
import type { Hub } from '@sentry/types';

import { _deserializeProvisionalIdentity, isSecretProvisionalIdentity } from './Identity';
import type { ClientOptions } from './Network/Client';
Expand Down Expand Up @@ -60,6 +62,9 @@ export type TankerCoreOptions = {
url?: string;
dataStore: DataStoreOptions;
sdkType: string;
/// Setting this to null explicitly turns off Sentry integration.
/// If left undefined, we try to find Sentry ourselves.
sentryHub?: Hub | null,
};

export type TankerOptions = Partial<Omit<TankerCoreOptions, 'dataStore'> & { dataStore: Partial<DataStoreOptions>; }>;
Expand Down Expand Up @@ -94,6 +99,7 @@ export class Tanker extends EventEmitter {
_clientOptions: ClientOptions;
_dataStoreOptions: DataStoreOptions;
_localDeviceLock: Lock;
_sentry: Hub | null;

static version = TANKER_SDK_VERSION;
static statuses = statuses;
Expand All @@ -118,6 +124,12 @@ export class Tanker extends EventEmitter {
throw new InvalidArgument('options.dataStore.adapter', 'function', options.dataStore.adapter);
}

if (options.sentryHub === undefined) {
this._sentry = globalThis.Sentry?.getCurrentHub() || null;
} else {
this._sentry = options.sentryHub;
}

assertNotEmptyString(options.sdkType, 'options.sdkType');
this._localDeviceLock = new Lock();
this._options = options;
Expand Down Expand Up @@ -172,8 +184,9 @@ export class Tanker extends EventEmitter {
}

get statusName(): string {
const def = statusDefs[this.status];
return def && def.name || `invalid status: ${this.status}`;
if (!this._session)
return statusDefs[statuses.STOPPED]!.name;
return this._session.statusName;
}

override addListener(eventName: string, listener: (...args: Array<unknown>) => void) {
Expand Down Expand Up @@ -271,7 +284,7 @@ export class Tanker extends EventEmitter {
const session = await Session.init(userData, async () => {
await this._initUnauthSession();
return this._unauthSession!.getOidcNonceManager();
}, this._dataStoreOptions, this._clientOptions);
}, this._dataStoreOptions, this._clientOptions, this._sentry);
// Watch and start the session
session.on('status_change', s => this.emit('statusChange', s));
await session.start();
Expand Down
44 changes: 44 additions & 0 deletions packages/core/src/__tests__/SentryLimiter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { expect } from '@tanker/test-utils';
import { BREADCRUMB_LIMIT, SentryLimiter } from '../SentryLimiter';
import type { Breadcrumb } from '@sentry/types';

describe('SentryLimiter', () => {
it('adds timestamp on breadcrumbs', async () => {
// @ts-expect-error Not using a real Hub object
const limiter = new SentryLimiter({
addBreadcrumb: (breadcrumb: Breadcrumb) => {
expect(breadcrumb.timestamp).to.be.a('number');
expect(breadcrumb.message).to.equal('plop');
},
});

limiter.addBreadcrumb({
message: 'plop',
// no timestamp
});
});

it('only keeps the last BREADCRUMB_LIMIT breadcrumbs', async () => {
let result: Array<Breadcrumb> = [];
// @ts-expect-error Not using a real Hub object
const limiter = new SentryLimiter({
addBreadcrumb: (b: Breadcrumb) => result.push(b),
});

const NUM_TO_DROP = 10;
for (let i = 0; i < BREADCRUMB_LIMIT + NUM_TO_DROP; i += 1) {
limiter.addBreadcrumb({
level: 'info',
message: `${i}`,
});
}
expect(result.length).to.equal(0);

limiter.flush();
expect(result.length).to.equal(BREADCRUMB_LIMIT);
result.forEach((b, idx) => {
expect(b.level).to.equal('info');
expect(b.message).to.equal(`${NUM_TO_DROP + idx}`);
});
});
});
1 change: 1 addition & 0 deletions packages/functional-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"package.json"
],
"dependencies": {
"@sentry/types": "7.76.0",
"@tanker/client-browser": "0.0.1",
"@tanker/client-node": "0.0.1",
"@tanker/core": "0.0.1",
Expand Down
5 changes: 3 additions & 2 deletions packages/functional-tests/src/__tests__/client.spec.node.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import Tanker from '@tanker/client-node';
import { pouchDBMemory } from '@tanker/datastore-pouchdb-memory';

import type { b64string } from '@tanker/core';
import type { TankerOptions, b64string } from '@tanker/core';

import type { DefaultDownloadType, TestResources } from '../helpers';
import { appdUrl, makeRandomUint8Array } from '../helpers';
import { generateFunctionalTests } from '..';

const makeTanker = (appId: b64string, storagePrefix: string): Tanker => {
const makeTanker = (appId: b64string, storagePrefix: string, extraOpts: TankerOptions): Tanker => {
const tanker = new Tanker({
appId,
dataStore: { adapter: pouchDBMemory, prefix: storagePrefix },
sdkType: 'js-functional-tests-node',
url: appdUrl,
...extraOpts,
});

return tanker;
Expand Down
Loading

0 comments on commit c2e6bf4

Please sign in to comment.