Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 2 additions & 2 deletions packages/browser/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ function startSessionTracking(): void {
return;
}

hub.startSession();
hub.startSession({ isBrowser: true });
hub.captureSession();

// We want to create a session for every navigation as well
Expand All @@ -209,7 +209,7 @@ function startSessionTracking(): void {
if (from === undefined || from === to) {
return;
}
hub.startSession();
hub.startSession({ isBrowser: true });
hub.captureSession();
},
type: 'history',
Expand Down
37 changes: 26 additions & 11 deletions packages/hub/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
SessionStatus,
Transport,
} from '@sentry/types';
import { dropUndefinedKeys, logger, uuid4 } from '@sentry/utils';
import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils';

import { getCurrentHub } from './hub';

Expand All @@ -21,15 +21,20 @@ export class Session implements SessionInterface {
public release?: string;
public sid: string = uuid4();
public did?: string;
public timestamp: number = Date.now();
public started: number = Date.now();
public duration: number = 0;
public timestamp: number;
public started: number;
public duration?: number = 0;
public status: SessionStatus = SessionStatus.Ok;
public environment?: string;
public ipAddress?: string;
public init: boolean = true;
public isBrowser: boolean = false;

public constructor(context?: Omit<SessionContext, 'started' | 'status'>) {
// Both timestamp and started are in seconds since the UNIX epoch.
const startingTime = timestampInSeconds();
this.timestamp = startingTime;
this.started = startingTime;
if (context) {
this.update(context);
}
Expand All @@ -48,8 +53,10 @@ export class Session implements SessionInterface {
}
}

this.timestamp = context.timestamp || Date.now();

this.timestamp = context.timestamp || timestampInSeconds();
if (context.isBrowser) {
this.isBrowser = context.isBrowser;
}
if (context.sid) {
// Good enough uuid validation. — Kamil
this.sid = context.sid.length === 32 ? context.sid : uuid4();
Expand All @@ -63,10 +70,17 @@ export class Session implements SessionInterface {
if (typeof context.started === 'number') {
this.started = context.started;
}
if (typeof context.duration === 'number') {
// The session duration for browser sessions does not track a meaningful
// concept that can be used as a metric.
// Automatically captured sessions are akin to page views, and thus we
// discard their duration.
if (this.isBrowser) {
this.duration = undefined;
} else if (typeof context.duration === 'number') {
this.duration = context.duration;
} else {
this.duration = this.timestamp - this.started;
const duration = this.timestamp - this.started;
this.duration = duration >= 0 ? duration : 0;
}
if (context.release) {
this.release = context.release;
Expand Down Expand Up @@ -106,7 +120,7 @@ export class Session implements SessionInterface {
did?: string;
timestamp: string;
started: string;
duration: number;
duration?: number;
status: SessionStatus;
errors: number;
attrs?: {
Expand All @@ -119,8 +133,9 @@ export class Session implements SessionInterface {
return dropUndefinedKeys({
sid: `${this.sid}`,
init: this.init,
started: new Date(this.started).toISOString(),
timestamp: new Date(this.timestamp).toISOString(),
// Make sure that sec is converted to ms for date constructor
started: new Date(this.started * 1000).toISOString(),
timestamp: new Date(this.timestamp * 1000).toISOString(),
status: this.status,
errors: this.errors,
did: typeof this.did === 'number' || typeof this.did === 'string' ? `${this.did}` : undefined,
Expand Down
117 changes: 117 additions & 0 deletions packages/hub/test/session.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { SessionContext, SessionStatus } from '@sentry/types';
import { timestampInSeconds } from '@sentry/utils';

import { Session } from '../src/session';

describe('Session', () => {
it('initializes with the proper defaults', () => {
const session = new Session().toJSON();

const sessionStartTime = session.timestamp;

expect(session).toEqual({
attrs: {},
duration: 0,
errors: 0,
init: true,
sid: expect.any(String),
started: expect.any(String),
status: SessionStatus.Ok,
timestamp: expect.any(String),
});

expect(session.sid).toHaveLength(32);

// started and timestamp should be the same on creation
expect(session.started).toEqual(sessionStartTime);
expect(session.timestamp).toEqual(sessionStartTime);
});

describe('update', () => {
const time = timestampInSeconds();
// [ name, in, out ]
const table: Array<[string, SessionContext, Record<string, any>]> = [
['sets an ip address', { user: { ip_address: '0.0.0.0' } }, { attrs: { ip_address: '0.0.0.0' } }],
['sets a did', { user: { id: 'specialID123' } }, { did: 'specialID123' }],
['sets a timestamp', { timestamp: time }, { timestamp: new Date(time * 1000).toISOString() }],
['sets a sid', { sid: '99705f22a3f1468e95ba7386e84691aa' }, { sid: '99705f22a3f1468e95ba7386e84691aa' }],
['requires custom sid to be of certain length', { sid: '123' }, { sid: expect.not.stringMatching('123') }],
['requires custom sid to be of certain length', { sid: '123' }, { sid: expect.not.stringMatching('123') }],
['sets an init', { init: false }, { init: false }],
['sets an did', { did: 'specialID123' }, { did: 'specialID123' }],
['overwrites user did with custom did', { did: 'custom-did', user: { id: 'user-id' } }, { did: 'custom-did' }],
['sets a started time', { started: time }, { started: new Date(time * 1000).toISOString() }],
['does not set a duration for browser env', { isBrowser: true }, { duration: undefined }],
['sets a duration', { duration: 12000 }, { duration: 12000 }],
['does not use custom duration for browser env', { duration: 12000, isBrowser: true }, { duration: undefined }],
[
'does not set a negative duration',
{ timestamp: 10, started: 100 },
// TODO(abhi): What should the behaviour here be?
{ duration: 0, timestamp: expect.any(String), started: expect.any(String) },
],
[
'sets duration based on timestamp and started',
{ timestamp: 100, started: 10 },
{ duration: 90, timestamp: expect.any(String), started: expect.any(String) },
],
[
'sets a release',
{ release: 'f1557994979ecd969963f53c27ca946379d721f3' },
{ attrs: { release: 'f1557994979ecd969963f53c27ca946379d721f3' } },
],
['sets an environment', { environment: 'staging' }, { attrs: { environment: 'staging' } }],
['sets an ipAddress', { ipAddress: '0.0.0.0' }, { attrs: { ip_address: '0.0.0.0' } }],
[
'overwrites user ip_address did with custom ipAddress',
{ ipAddress: '0.0.0.0', user: { ip_address: '1.1.1.1' } },
{ attrs: { ip_address: '0.0.0.0' } },
],
['sets an userAgent', { userAgent: 'Mozilla/5.0' }, { attrs: { user_agent: 'Mozilla/5.0' } }],
['sets errors', { errors: 3 }, { errors: 3 }],
['sets status', { status: SessionStatus.Crashed }, { status: SessionStatus.Crashed }],
];

test.each(table)('%s', (...test) => {
// duration and timestamp can vary after session update, so let's expect anything unless
// the out variable in a test explicitly refers to it.
const DEFAULT_OUT = { duration: expect.any(Number), timestamp: expect.any(String) };

const session = new Session();
const initSessionProps = session.toJSON();

session.update(test[1]);
expect(session.toJSON()).toEqual({ ...initSessionProps, ...DEFAULT_OUT, ...test[2] });
});
});

describe('close', () => {
it('exits a normal session', () => {
const session = new Session();
const mockUpdate = jest.spyOn(session, 'update');
expect(mockUpdate).toHaveBeenCalledTimes(0);
session.close();
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdate).toHaveBeenLastCalledWith({ status: SessionStatus.Exited });
});

it('updates session status when give status', () => {
const session = new Session();
const mockUpdate = jest.spyOn(session, 'update');

session.close(SessionStatus.Crashed);
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdate).toHaveBeenLastCalledWith({ status: SessionStatus.Crashed });
});

it('only changes status ok', () => {
const session = new Session();
session.status = SessionStatus.Abnormal;
const mockUpdate = jest.spyOn(session, 'update');

session.close();
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdate).toHaveBeenLastCalledWith();
});
});
});
6 changes: 5 additions & 1 deletion packages/types/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface Session extends SessionContext {
did?: string;
timestamp: string;
started: string;
duration: number;
duration?: number;
status: SessionStatus;
errors: number;
attrs?: {
Expand All @@ -40,7 +40,9 @@ export interface SessionContext {
sid?: string;
did?: string;
init?: boolean;
// seconds since the UNIX epoch
timestamp?: number;
// seconds since the UNIX epoch
started?: number;
duration?: number;
status?: SessionStatus;
Expand All @@ -50,6 +52,8 @@ export interface SessionContext {
ipAddress?: string;
errors?: number;
user?: User | null;
// Sessions are handled differently in browser environments
isBrowser?: boolean;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const timestampSource: TimestampSource =
/**
* Returns a timestamp in seconds since the UNIX epoch using the Date API.
*/
export const dateTimestampInSeconds = dateTimestampSource.nowSeconds.bind(dateTimestampSource);
export const dateTimestampInSeconds: () => number = dateTimestampSource.nowSeconds.bind(dateTimestampSource);

/**
* Returns a timestamp in seconds since the UNIX epoch using either the Performance or Date APIs, depending on the
Expand All @@ -116,7 +116,7 @@ export const dateTimestampInSeconds = dateTimestampSource.nowSeconds.bind(dateTi
* skew can grow to arbitrary amounts like days, weeks or months.
* See https://github.com/getsentry/sentry-javascript/issues/2590.
*/
export const timestampInSeconds = timestampSource.nowSeconds.bind(timestampSource);
export const timestampInSeconds: () => number = timestampSource.nowSeconds.bind(timestampSource);

// Re-exported with an old name for backwards-compatibility.
export const timestampWithMs = timestampInSeconds;
Expand Down