Skip to content

Commit

Permalink
disableDiagnostics only disable core api logging (#411)
Browse files Browse the repository at this point in the history
Change:
Send initialize diagnostics event if disableDiagnostics is set to be
true

---------

Co-authored-by: sroyal-statsig <[email protected]>
Co-authored-by: Daniel <[email protected]>
  • Loading branch information
3 people authored Dec 21, 2023
1 parent 5f0ca9f commit c18f227
Show file tree
Hide file tree
Showing 14 changed files with 208 additions and 54 deletions.
31 changes: 18 additions & 13 deletions src/ErrorBoundary.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { v4 as uuidv4 } from 'uuid';
import {
StatsigUninitializedError,
StatsigInvalidArgumentError,
StatsigInitializationTimeoutError,
} from './Errors';
import StatsigSDKOptions from './StatsigSDKOptions';
import Diagnostics from './utils/Diagnostics';
import parseError from './utils/parseError';
export const ExceptionEndpoint = 'https://statsigapi.net/v1/sdk_exception';
Expand All @@ -21,7 +23,7 @@ export default class ErrorBoundary {
private statsigMetadata?: Record<string, string | number>;
private seen = new Set<string>();

constructor(private sdkKey: string) {
constructor(private sdkKey: string, private sdkOptions: StatsigSDKOptions ) {
const sampling = Math.floor(Math.random() * SAMPLING_RATE);
this.setupDiagnostics(sampling === 0 ? MAX_DIAGNOSTICS_MARKERS : 0);
}
Expand All @@ -45,7 +47,7 @@ export default class ErrorBoundary {
tag: string,
task: () => T,
recover: () => T,
{ getExtraData, configName }: CaptureOptions = {},
captureOptions: CaptureOptions = {},
): T {
let markerID: string | null = null;
try {
Expand All @@ -57,43 +59,47 @@ export default class ErrorBoundary {
return result
.catch((e: unknown) => {
wasSuccessful = false;
return this.onCaught(tag, e, recover, getExtraData);
return this.onCaught(tag, e, recover, captureOptions);
})
.then((possiblyRecoveredResult) => {
this.endMarker(tag, wasSuccessful, markerID);
return possiblyRecoveredResult;
}) as unknown as T;
}

this.endMarker(tag, true, markerID, configName);
this.endMarker(tag, true, markerID, captureOptions.configName);
return result;
} catch (error) {
this.endMarker(tag, false, markerID, configName);
return this.onCaught(tag, error, recover, getExtraData);
this.endMarker(tag, false, markerID, captureOptions.configName);
return this.onCaught(tag, error, recover, captureOptions);
}
}

public logError(
tag: string,
error: unknown,
getExtraData?: ExtraDataExtractor,
{ getExtraData, configName }: CaptureOptions = {}
): void {
(async () => {
try {
const extra =
typeof getExtraData === 'function' ? await getExtraData() : null;
typeof getExtraData === 'function' ? await getExtraData() : {};
const { name, trace: info } = parseError(error);

extra["configName"] = configName
if (this.seen.has(name)) return;
this.seen.add(name);

const metadata = this.statsigMetadata ?? {};
if(metadata.sessionID == null) {
metadata.sessionID = uuidv4()
}
const body = JSON.stringify({
tag,
exception: name,
info,
statsigMetadata: metadata,
extra: extra ?? {},
statsigOptions: this.sdkOptions.getLoggingCopy(),
extra: extra
});
return fetch(ExceptionEndpoint, {
method: 'POST',
Expand Down Expand Up @@ -157,7 +163,7 @@ export default class ErrorBoundary {
tag: string,
error: unknown,
recover: () => T,
getExtraData?: ExtraDataExtractor,
captureOptions: CaptureOptions = {}
): T {
if (
error instanceof StatsigUninitializedError ||
Expand All @@ -172,8 +178,7 @@ export default class ErrorBoundary {
}

console.error('[Statsig] An unexpected exception occurred.', error);

this.logError(tag, error, getExtraData);
this.logError(tag, error, captureOptions);

return recover();
}
Expand Down
2 changes: 1 addition & 1 deletion src/StatsigClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ export default class StatsigClient implements IHasStatsigInternal, IStatsig {
Diagnostics.initialize({
options: this.options,
});
this.errorBoundary = new ErrorBoundary(sdkKey);
this.errorBoundary = new ErrorBoundary(sdkKey, this.options);
this.ready = false;
this.sdkKey = sdkKey;
this.consoleLogger = new ConsoleLogger(this.options.getLogLevel());
Expand Down
1 change: 1 addition & 0 deletions src/StatsigIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type StatsigMetadata = {
sdkType: string;
sdkVersion: string;
stableID?: string;
sessionID?: string;
locale?: string;
appVersion?: string;
systemVersion?: string;
Expand Down
23 changes: 14 additions & 9 deletions src/StatsigLogger.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { v4 as uuidv4 } from 'uuid';
import LogEvent from './LogEvent';
import { IHasStatsigInternal } from './StatsigClient';
import { StatsigEndpoint } from './StatsigNetwork';
Expand Down Expand Up @@ -291,14 +292,15 @@ export default class StatsigLogger {
}

public logDiagnostics(user: StatsigUser | null, context: ContextType) {
if (Diagnostics.disabled) {
return;
}
const markers = Diagnostics.getMarkers(context);
if (markers.length <= 0) {
return
}
Diagnostics.clearContext(context);
const event = this.makeDiagnosticsEvent(user, {
markers,
context,
statsigOptions: this.sdkInternal.getOptions().getLoggingCopy()
});
this.log(event);
}
Expand Down Expand Up @@ -392,7 +394,10 @@ export default class StatsigLogger {
if (this.queue.length === 0) {
return;
}

const statsigMetadata = this.sdkInternal.getStatsigMetadata()
if(statsigMetadata.sessionID == null) {
statsigMetadata.sessionID = uuidv4()
}
const oldQueue = this.queue;
this.queue = [];
if (
Expand Down Expand Up @@ -446,22 +451,22 @@ export default class StatsigLogger {
error.text().then((errorText: string) => {
this.sdkInternal
.getErrorBoundary()
.logError(LOG_FAILURE_EVENT, error, async () => {
.logError(LOG_FAILURE_EVENT, error, {getExtraData:async () => {
return {
eventCount: oldQueue.length,
error: errorText,
};
};}
});
});
} else {
this.sdkInternal
.getErrorBoundary()
.logError(LOG_FAILURE_EVENT, error, async () => {
.logError(LOG_FAILURE_EVENT, error, {getExtraData: async () => {
return {
eventCount: oldQueue.length,
error: error.message,
};
});
}});
}
this.newFailedRequest(LOG_FAILURE_EVENT, oldQueue);
})
Expand Down Expand Up @@ -607,7 +612,7 @@ export default class StatsigLogger {

private makeDiagnosticsEvent(
user: StatsigUser | null,
data: { context: ContextType; markers: Marker[] },
data: { context: ContextType; markers: Marker[], statsigOptions?: Record<string, unknown> },
) {
const latencyEvent = new LogEvent(DIAGNOSTICS_EVENT);
latencyEvent.setUser(user);
Expand Down
4 changes: 2 additions & 2 deletions src/StatsigNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,15 @@ export default class StatsigNetwork {
);
this.sdkInternal
.getErrorBoundary()
.logError('postWithTimeoutInvalidRes', error, async () => {
.logError('postWithTimeoutInvalidRes', error, {getExtraData: async () => {
return this.getErrorData(
endpointName,
body,
retries,
backoff,
res,
);
});
}});
return Promise.reject(error);
}

Expand Down
46 changes: 46 additions & 0 deletions src/StatsigSDKOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export default class StatsigSDKOptions {
private prefetchUsers: StatsigUser[];
private updateCompletionCallback: UpdateUserCompletionCallback | null;
private userPersistentStorage: UserPersistentStorageInterface | null;

private loggingCopy: Record<string, unknown> | undefined;

constructor(options?: StatsigOptions | null) {
if (options == null) {
Expand Down Expand Up @@ -158,6 +160,49 @@ export default class StatsigSDKOptions {
this.gateEvaluationCallback = options?.gateEvaluationCallback ?? null;
this.userPersistentStorage = options?.userPersistentStorage ?? null;
this.disableAllLogging = options.disableAllLogging ?? false;
this.setLoggingCopy(options)
}

setLoggingCopy(options: StatsigOptions | null) {
if(options == null || this.loggingCopy != null) {
return
}
const loggingCopy: Record<string, unknown> = {}
Object.entries(options).forEach(([option,value]) => {
const valueType = typeof value
switch (valueType) {
case "number":
case "bigint":
case "boolean":
loggingCopy[String(option)] = value
break
case "string":
if((value as string).length < 50) {
loggingCopy[String(option)] = value
} else {
loggingCopy[String(option)] = "set"
}
break
case "object":
if(option === "environment") {
loggingCopy["environment"] = value
} else if (option === "prefetchUsers") {
loggingCopy["prefetchUsers"] = (options.prefetchUsers?.length ?? 0) > 0
} else {
loggingCopy[String(option)] = (value != null) ? "set" : "unset"
}
break
case "function":
if(option === "userPersistentStorage") {
loggingCopy["userPersistentStorage"] = (value != null) ? "set" : "unset"
}
}
})
this.loggingCopy = loggingCopy
}

getLoggingCopy(): Record<string, unknown> | undefined{
return this.loggingCopy
}

getApi(): string {
Expand Down Expand Up @@ -273,4 +318,5 @@ export default class StatsigSDKOptions {
}
return Math.max(Math.min(input, bounds.max), bounds.min);
}

}
9 changes: 9 additions & 0 deletions src/__tests__/AsyncInitVsUpdate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,15 @@ describe('Race conditions between initializeAsync and updateUser', () => {
},
user: { userID: 'user-b', customIDs: { workID: 'employee-b' } },
}),
expect.objectContaining({
eventName: 'statsig::diagnostics',
metadata: {
markers: expect.any(Array),
context: 'initialize',
statsigOptions: expect.any(Object),
},
user: { userID: 'user-a', customIDs: { workID: 'employee-a' } },
}),
expect.objectContaining({
eventName: 'statsig::config_exposure',
metadata: {
Expand Down
49 changes: 42 additions & 7 deletions src/__tests__/ErrorBoundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
StatsigInvalidArgumentError,
StatsigUninitializedError,
} from '../Errors';
import StatsigSDKOptions from '../StatsigSDKOptions';
import StatsigSDKOptions, {
UserPersistentStorageInterface,
} from '../StatsigSDKOptions';
import Diagnostics from '../utils/Diagnostics';

type ErrorBoundaryRequest = {
Expand All @@ -15,8 +17,19 @@ type ErrorBoundaryRequest = {
};
};

class FakeUserPersistentStorage implements UserPersistentStorageInterface {
userIDType: string | undefined;
load(key: string): string {
return '';
}
save(key: string, data: string) {
// no-op
}
}

describe('ErrorBoundary', () => {
let boundary: ErrorBoundary;
let options: StatsigSDKOptions;
let request: ErrorBoundaryRequest[] = [
{
url: '',
Expand All @@ -27,10 +40,16 @@ describe('ErrorBoundary', () => {
];

beforeEach(() => {
Diagnostics.initialize({
options: new StatsigSDKOptions(),
options = new StatsigSDKOptions({
api: 'www.google.com',
disableAllLogging: true,
loggingBufferMaxSize: 2046,
initializeValues: { feature_gates: [] },
userPersistentStorage: new FakeUserPersistentStorage(),
initCompletionCallback: () => {},
});
boundary = new ErrorBoundary('client-key');
Diagnostics.initialize({ options });
boundary = new ErrorBoundary('client-key', options);
request = [];

// @ts-ignore
Expand Down Expand Up @@ -88,16 +107,32 @@ describe('ErrorBoundary', () => {

it('logs errors correctly', () => {
const err = new URIError();
boundary.swallow('', () => {
throw err;
});
boundary.swallow(
'',
() => {
throw err;
},
{
configName: 'fake_gate',
},
);

expect(request[0].url).toEqual(ExceptionEndpoint);

expect(JSON.parse(request[0].params['body'])).toEqual(
expect.objectContaining({
exception: 'URIError',
info: err.stack,
statsigOptions: {
api: 'www.google.com',
disableAllLogging: true,
loggingBufferMaxSize: 2046,
initializeValues: "set",
userPersistentStorage: "set",
},
extra: {
configName: 'fake_gate',
},
}),
);
});
Expand Down
Loading

0 comments on commit c18f227

Please sign in to comment.