Skip to content
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
47 changes: 41 additions & 6 deletions __tests__/Logger-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ test('Test Log.info()', () => {
delete packet[0].timestamp;
delete packet[1].timestamp;
expect(packet).toEqual([
{message: "[info] Test1", parameters: "",},
{message: "[info] Test2", parameters: "",},
{message: "[info] Test1", parameters: "", email: null},
{message: "[info] Test2", parameters: "", email: null},
]);

// Test the case where `isDebug` is `true` in `Log` instance and we pass `extraData` parameter
Expand All @@ -53,7 +53,7 @@ test('Test Log.alert()', () => {
);
const packet = JSON.parse(mockServerLoggingCallback.mock.calls[0][1].logPacket);
delete packet[0].timestamp;
expect(packet).toEqual([{message: "[alrt] Test2", parameters: {}}]);
expect(packet).toEqual([{message: "[alrt] Test2", parameters: {}, email: null}]);
});

test('Test Log.warn()', () => {
Expand All @@ -69,7 +69,7 @@ test('Test Log.warn()', () => {
);
const packet = JSON.parse(mockServerLoggingCallback.mock.calls[0][1].logPacket);
delete packet[0].timestamp;
expect(packet).toEqual([{message: "[warn] Test2", parameters: ''}]);
expect(packet).toEqual([{message: "[warn] Test2", parameters: '', email: null}]);
});

test('Test Log.hmmm()', () => {
Expand All @@ -88,8 +88,8 @@ test('Test Log.hmmm()', () => {
delete packet[0].timestamp;
delete packet[1].timestamp;
expect(packet).toEqual([
{message: "[hmmm] Test", parameters: ''},
{message: "[info] Test", parameters: ''}
{message: "[hmmm] Test", parameters: '', email: null},
{message: "[info] Test", parameters: '', email: null}
]);
});

Expand All @@ -98,3 +98,38 @@ test('Test Log.client()', () => {
expect(mockClientLoggingCallback).toHaveBeenCalled();
expect(mockClientLoggingCallback).toHaveBeenCalledWith('Test', '');
});

test('Test getContextEmail captures email per log line', () => {
const mockCallback = jest.fn();
const LogWithEmail = new Logger({
serverLoggingCallback: mockCallback,
clientLoggingCallback: jest.fn(),
getContextEmail: () => 'test@example.com',
});

LogWithEmail.info('Test message', true);
expect(mockCallback).toHaveBeenCalled();

const packet = JSON.parse(mockCallback.mock.calls[0][1].logPacket);
delete packet[0].timestamp;
expect(packet).toEqual([{message: "[info] Test message", parameters: '', email: 'test@example.com'}]);
});

test('Test getContextEmail throwing does not break logging', () => {
const mockCallback = jest.fn();
const LogWithThrowingEmail = new Logger({
serverLoggingCallback: mockCallback,
clientLoggingCallback: jest.fn(),
getContextEmail: () => {
throw new Error('Failed to get email');
},
});

// Should not throw
LogWithThrowingEmail.info('Test message', true);
expect(mockCallback).toHaveBeenCalled();

const packet = JSON.parse(mockCallback.mock.calls[0][1].logPacket);
delete packet[0].timestamp;
expect(packet).toEqual([{message: "[info] Test message", parameters: '', email: null}]);
});
25 changes: 22 additions & 3 deletions lib/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ type Parameters = string | Record<string, unknown> | Array<Record<string, unknow
type ServerLoggingCallbackOptions = {api_setCookie: boolean; logPacket: string};
type ServerLoggingCallback = (logger: Logger, options: ServerLoggingCallbackOptions) => Promise<{requestID: string}> | undefined;
type ClientLoggingCallBack = (message: string, extraData: Parameters) => void;
type LogLine = {message: string; parameters: Parameters; onlyFlushWithOthers?: boolean; timestamp: Date};
type LoggerOptions = {serverLoggingCallback: ServerLoggingCallback; isDebug: boolean; clientLoggingCallback: ClientLoggingCallBack; maxLogLinesBeforeFlush?: number};
type LogLine = {message: string; parameters: Parameters; onlyFlushWithOthers?: boolean; timestamp: Date; email?: string | null};
type LoggerOptions = {
serverLoggingCallback: ServerLoggingCallback;
isDebug: boolean;
clientLoggingCallback: ClientLoggingCallBack;
maxLogLinesBeforeFlush?: number;
getContextEmail?: () => string | null;
};

const MAX_LOG_LINES_BEFORE_FLUSH = 50;

Expand All @@ -18,13 +24,16 @@ export default class Logger {

maxLogLinesBeforeFlush: number;

constructor({serverLoggingCallback, isDebug, clientLoggingCallback, maxLogLinesBeforeFlush}: LoggerOptions) {
getContextEmail?: () => string | null;

constructor({serverLoggingCallback, isDebug, clientLoggingCallback, maxLogLinesBeforeFlush, getContextEmail}: LoggerOptions) {
// An array of log lines that limits itself to a certain number of entries (deleting the oldest)
this.logLines = [];
this.serverLoggingCallback = serverLoggingCallback;
this.clientLoggingCallback = clientLoggingCallback;
this.isDebug = isDebug;
this.maxLogLinesBeforeFlush = maxLogLinesBeforeFlush || MAX_LOG_LINES_BEFORE_FLUSH;
this.getContextEmail = getContextEmail;

// Public Methods
this.info = this.info.bind(this);
Expand Down Expand Up @@ -70,11 +79,21 @@ export default class Logger {
* @param onlyFlushWithOthers A request will never be sent to the server if all loglines have this set to true
*/
add(message: string, parameters: Parameters, forceFlushToServer: boolean, onlyFlushWithOthers = false, extraData: Parameters = '') {
// Capture the user's email at the moment this specific log line is created
// This ensures the log retains user context even if the session is cleared before sending
let email: string | null = null;
try {
email = this.getContextEmail ? this.getContextEmail() : null;
} catch {
// Silently fail if getContextEmail throws - logging should not crash
}

const length = this.logLines.push({
message,
parameters,
onlyFlushWithOthers,
timestamp: new Date(),
email,
});

if (this.isDebug) {
Expand Down