Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Try sending metrics to panorama before clog API #109

Merged
merged 1 commit into from
Jan 7, 2025
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
130 changes: 77 additions & 53 deletions src/internal/base-component/__tests__/metrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ describe('Client Metrics support', () => {
const metrics = new Metrics('dummy-package', '1.0');

const checkMetric = (metricName: string, detailObject: MetricDetail) => {
expect(window.AWSC.Clog.log).toHaveBeenCalledWith(metricName, 1, JSON.stringify(detailObject));
expect(window.AWSC.Clog.log).toHaveBeenCalledTimes(1);
expect(window.panorama).toHaveBeenCalledWith('trackCustomEvent', {
eventName: metricName,
eventDetail: JSON.stringify(detailObject),
eventValue: '1',
timestamp: expect.any(Number),
});
expect(window.panorama).toHaveBeenCalledTimes(1);
};

beforeEach(() => {
Expand All @@ -49,31 +54,16 @@ describe('Client Metrics support', () => {
});

describe('sendMetric', () => {
test('does nothing when window.AWSC is undefined', () => {
test('does nothing of both AWSC and panorama APIs are not available', () => {
delete window.panorama;
delete window.AWSC;
metrics.sendMetric('name', 0); // only proves no exception thrown
});

test('does nothing when window.AWSC.Clog is undefined', () => {
window.AWSC = {};
metrics.sendMetric('name', 0); // only proves no exception thrown
expect(() => metrics.sendMetric('name', 0)).not.toThrow();
});

test('uses panorama API as fallback when AWSC.Clog.log is unavailable', () => {
delete window.AWSC;
test('uses AWSC.Clog.log API as fallback when panorama is unavailable', () => {
delete window.panorama;
metrics.sendMetric('name', 0);
expect(window.panorama).toHaveBeenCalledWith('trackCustomEvent', {
eventName: 'name',
eventValue: '0',
timestamp: expect.any(Number),
});
});

test('does nothing when window.AWSC.Clog.log is undefined', () => {
window.AWSC = {
Clog: undefined,
};
metrics.sendMetric('name', 0); // only proves no exception thrown
expect(window.AWSC.Clog.log).toHaveBeenCalledWith('name', 0, undefined);
});

describe('within an iframe', () => {
Expand All @@ -82,51 +72,60 @@ describe('Client Metrics support', () => {

afterEach(() => {
Object.defineProperty(window, 'parent', originalWindowParent);
expect(window.parent.AWSC).toBeUndefined();
expect(window.parent.panorama).toBeUndefined();
});

const setupIframe = () => {
Object.defineProperty(window, 'parent', { configurable: true, writable: true, value: { parent: {} } });
};

test('does nothing when AWSC is not defined in the parent iframe', () => {
delete window.AWSC;
test('does nothing when window.panorama is not defined in the parent iframe', () => {
delete window.panorama;
setupIframe();
expect(window.parent.AWSC).toBeUndefined();
expect(window.parent.panorama).toBeUndefined();

metrics.sendMetric('name', 0); // only proves no exception thrown
});

test('works if parent has AWSC', () => {
test('works if window.parent has panorama object', () => {
setupIframe();
delete window.AWSC;
window.parent.AWSC = {
Clog: {
log: () => {},
},
};
jest.spyOn(window.parent.AWSC.Clog, 'log');
expect(window.AWSC).toBeUndefined();
expect(window.parent.AWSC).toBeDefined();
delete window.panorama;
window.parent.panorama = () => {};
jest.spyOn(window.parent, 'panorama');
expect(window.panorama).toBeUndefined();
expect(window.parent.panorama).toBeDefined();

metrics.sendMetric('name', 0, undefined);
expect(window.parent.AWSC.Clog.log).toHaveBeenCalledWith('name', 0, undefined);
expect(window.parent.panorama).toHaveBeenCalledWith('trackCustomEvent', {
eventName: 'name',
eventValue: '0',
timestamp: expect.any(Number),
});
});
});

describe('when window.AWSC.Clog.log is defined', () => {
describe('when window.panorama is defined', () => {
mockConsoleError();

test('delegates to window.AWSC.Clog.log when defined', () => {
test('delegates to window.panorama when defined', () => {
metrics.sendMetric('name', 0, undefined);
expect(window.AWSC.Clog.log).toHaveBeenCalledWith('name', 0, undefined);
expect(window.panorama).toHaveBeenCalledWith('trackCustomEvent', {
eventName: 'name',
eventValue: '0',
timestamp: expect.any(Number),
});
});

describe('Metric name validation', () => {
const tryValidMetric = (metricName: string) => {
test(`calls AWSC.Clog.log when valid metric name used (${metricName})`, () => {
test(`calls window.panorama when valid metric name used (${metricName})`, () => {
metrics.sendMetric(metricName, 1, 'detail');
expect(window.AWSC.Clog.log).toHaveBeenCalledWith(metricName, 1, 'detail');
expect(window.panorama).toHaveBeenCalledWith('trackCustomEvent', {
eventName: metricName,
eventValue: '1',
eventDetail: 'detail',
timestamp: expect.any(Number),
});
});
};

Expand All @@ -135,6 +134,8 @@ describe('Client Metrics support', () => {
metrics.sendMetric(metricName, 0, 'detail');
expect(console.error).toHaveBeenCalledWith(`Invalid metric name: ${metricName}`);
jest.mocked(console.error).mockReset();
expect(window.panorama).not.toHaveBeenCalled();
expect(window.AWSC.Clog.log).not.toHaveBeenCalled();
});
};

Expand All @@ -161,14 +162,21 @@ describe('Client Metrics support', () => {
test('accepts details below the character limit', () => {
const validDetail = 'a'.repeat(4000);
metrics.sendMetric('metricName', 1, validDetail);
expect(window.AWSC.Clog.log).toHaveBeenCalledWith('metricName', 1, validDetail);
expect(window.panorama).toHaveBeenCalledWith('trackCustomEvent', {
eventName: 'metricName',
eventValue: '1',
eventDetail: validDetail,
timestamp: expect.any(Number),
});
});

test('throws an error when detail is too long', () => {
const invalidDetail = 'a'.repeat(4001);
metrics.sendMetric('metricName', 0, invalidDetail);
expect(console.error).toHaveBeenCalledWith(`Detail for metric metricName is too long: ${invalidDetail}`);
jest.mocked(console.error).mockReset();
expect(window.panorama).not.toHaveBeenCalled();
expect(window.AWSC.Clog.log).not.toHaveBeenCalled();
});
});
});
Expand All @@ -177,15 +185,31 @@ describe('Client Metrics support', () => {
describe('sendMetricOnce', () => {
test('logs a metric name only once', () => {
metrics.sendMetricOnce('my-event', 1);
expect(window.AWSC.Clog.log).toHaveBeenCalledWith('my-event', 1, undefined);
expect(window.AWSC.Clog.log).toHaveBeenCalledTimes(1);
expect(window.panorama).toHaveBeenCalledWith('trackCustomEvent', {
eventName: 'my-event',
eventValue: '1',
timestamp: expect.any(Number),
});
expect(window.panorama).toHaveBeenCalledTimes(1);

metrics.sendMetricOnce('my-event', 2);
expect(window.AWSC.Clog.log).toHaveBeenCalledTimes(1);
expect(window.panorama).toHaveBeenCalledTimes(1);
});

metrics.sendMetricOnce('My-Event', 3);
expect(window.AWSC.Clog.log).toHaveBeenCalledWith('My-Event', 3, undefined);
expect(window.AWSC.Clog.log).toHaveBeenCalledTimes(2);
test('does not deduplicate metrics in different casing', () => {
metrics.sendMetricOnce('my-event', 1);
metrics.sendMetricOnce('My-Event', 2);
expect(window.panorama).toHaveBeenCalledWith('trackCustomEvent', {
eventName: 'my-event',
eventValue: '1',
timestamp: expect.any(Number),
});
expect(window.panorama).toHaveBeenCalledWith('trackCustomEvent', {
eventName: 'My-Event',
eventValue: '2',
timestamp: expect.any(Number),
});
expect(window.panorama).toHaveBeenCalledTimes(2);
});
});

Expand Down Expand Up @@ -265,7 +289,7 @@ describe('Client Metrics support', () => {

metrics.sendMetricObjectOnce(metricObj, 1);
metrics.sendMetricObjectOnce(metricObj, 1);
expect(window.AWSC.Clog.log).toHaveBeenCalledTimes(1);
expect(window.panorama).toHaveBeenCalledTimes(1);
});
test('logs metric for each different version if same source and action', () => {
metrics.sendMetricObjectOnce(
Expand All @@ -284,7 +308,7 @@ describe('Client Metrics support', () => {
},
1
);
expect(window.AWSC.Clog.log).toHaveBeenCalledTimes(2);
expect(window.panorama).toHaveBeenCalledTimes(2);
});
test('logs a metric multiple times if same source but different actions', () => {
metrics.sendMetricObjectOnce(
Expand All @@ -303,7 +327,7 @@ describe('Client Metrics support', () => {
},
1
);
expect(window.AWSC.Clog.log).toHaveBeenCalledTimes(2);
expect(window.panorama).toHaveBeenCalledTimes(2);
});
});

Expand Down
38 changes: 21 additions & 17 deletions src/internal/base-component/metrics/log-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,18 @@ export class CLogClient {
console.error(`Detail for metric ${metricName} is too long: ${detail}`);
return;
}
const wasSent = new PanoramaClient().sendMetric({
eventName: metricName,
eventDetail: detail,
eventValue: `${value}`,
timestamp: Date.now(),
});
if (wasSent) {
return;
}
const AWSC = this.findAWSC(window);
if (typeof AWSC === 'object' && typeof AWSC.Clog === 'object' && typeof AWSC.Clog.log === 'function') {
AWSC.Clog.log(metricName, value, detail);
} else {
new PanoramaClient().sendMetric({
eventName: metricName,
eventDetail: detail,
eventValue: `${value}`,
timestamp: Date.now(),
});
}
}

Expand Down Expand Up @@ -82,7 +84,11 @@ export class PanoramaClient {
/**
* Sends metric but only if Console Platform client v2 logging JS API is present in the page.
*/
sendMetric(metric: MetricsV2EventItem): void {
sendMetric(metric: MetricsV2EventItem): boolean {
const panorama = this.findPanorama(window);
if (typeof panorama !== 'function') {
return false;
}
if (typeof metric.eventDetail === 'object') {
metric.eventDetail = JSON.stringify(metric.eventDetail);
}
Expand All @@ -91,28 +97,26 @@ export class PanoramaClient {
}
if (!validateLength(metric.eventName, 1000)) {
console.error(`Event name for metric is too long: ${metric.eventName}`);
return;
return true;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation constraints are same for both API, return true to prevent it from attempting to use AWSC.Clog.log if the validation fails

}
if (!validateLength(metric.eventDetail, 4000)) {
console.error(`Event detail for metric is too long: ${metric.eventDetail}`);
return;
return true;
}
if (!validateLength(metric.eventValue, 4000)) {
console.error(`Event value for metric is too long: ${metric.eventValue}`);
return;
return true;
}
if (!validateLength(metric.eventContext, 4000)) {
console.error(`Event context for metric is too long: ${metric.eventContext}`);
return;
return true;
}
if (!validateLength(metric.eventType, 50)) {
console.error(`Event type for metric is too long: ${metric.eventType}`);
return;
}
const panorama = this.findPanorama(window);
if (typeof panorama === 'function') {
panorama('trackCustomEvent', { timestamp: Date.now(), ...metric });
return true;
}
panorama('trackCustomEvent', { timestamp: Date.now(), ...metric });
return true;
}

private findPanorama(currentWindow?: MetricsWindow): any | undefined {
Expand Down
Loading