Skip to content

Commit

Permalink
[Main][Task]26079397: Add disableBeaconSplit sender config and fix po…
Browse files Browse the repository at this point in the history
…tential duplicated events during unload/fallback sender (#2208) (#2210)

* [Main][Task]26079397: Add disableBeaconSplit sender config and fix potential duplicated events during unload/fallback sender (#2208)

* fix sendbeacon sender

* update

* Add release-3.0 branch to the actions

---------

Co-authored-by: Karlie-777 <79606506+Karlie-777@users.noreply.github.com>
  • Loading branch information
MSNev and Karlie-777 committed Dec 7, 2023
1 parent 9dda05c commit f471839
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 29 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Node.js CI

on:
push:
branches: [ main, master, beta, Release2.7 ]
branches: [ main, master, beta, Release2.7, release-3.0 ]
pull_request:
branches: [ main, master, beta, Release2.7 ]
branches: [ main, master, beta, Release2.7, release-3.0 ]

jobs:
build:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ name: "CodeQL"

on:
push:
branches: [ main, master, beta, Release2.7 ]
branches: [ main, master, beta, Release2.7, release-3.0 ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main, master, beta, Release2.7 ]
branches: [ main, master, beta, Release2.7, release-3.0 ]
schedule:
- cron: '17 15 * * 2'

Expand Down
273 changes: 270 additions & 3 deletions channels/applicationinsights-channel-js/Tests/Unit/src/Sender.tests.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { AITestClass, Assert } from "@microsoft/ai-test-framework";
import { AITestClass } from "@microsoft/ai-test-framework";
import { Sender } from "../../../src/Sender";
import { createOfflineListener, IOfflineListener } from '../../../src/Offline';
import { EnvelopeCreator } from '../../../src/EnvelopeCreator';
import { Exception, CtxTagKeys, isBeaconApiSupported, DEFAULT_BREEZE_ENDPOINT, DEFAULT_BREEZE_PATH, utlCanUseSessionStorage, utlGetSessionStorage, utlSetSessionStorage } from "@microsoft/applicationinsights-common";
import { ITelemetryItem, AppInsightsCore, ITelemetryPlugin, DiagnosticLogger, NotificationManager, SendRequestReason, _eInternalMessageId, getGlobalInst, safeGetLogger, getJSON, isString, isArray, arrForEach, isBeaconsSupported, IXHROverride, IPayloadData, isFetchSupported} from "@microsoft/applicationinsights-core-js";
import { ITelemetryItem, AppInsightsCore, ITelemetryPlugin, DiagnosticLogger, NotificationManager, SendRequestReason, _eInternalMessageId, getGlobalInst, safeGetLogger, getJSON, isString, isArray, arrForEach, isBeaconsSupported, IXHROverride, IPayloadData, isFetchSupported, getWindow} from "@microsoft/applicationinsights-core-js";
import { ArraySendBuffer, SessionStorageSendBuffer } from "../../../src/SendBuffer";
import { ISenderConfig } from "../../../src/Interfaces";

Expand Down Expand Up @@ -120,6 +120,7 @@ export class SenderTests extends AITestClass {
QUnit.assert.equal(10000, defaultSenderConfig.eventsLimitInMem, "Channel default eventsLimitInMem config is set");
QUnit.assert.equal(undefined, defaultSenderConfig.httpXHROverride, "Channel default httpXHROverride config is set");
QUnit.assert.equal(false, defaultSenderConfig.alwaysUseXhrOverride, "Channel default alwaysUseXhrOverride config is set");
QUnit.assert.equal(false, defaultSenderConfig.disableSendBeaconSplit, "Channel default disableSendBeaconSplit config is set");

//check dynamic config
core.config.extensionConfig = core.config.extensionConfig? core.config.extensionConfig : {};
Expand All @@ -134,7 +135,8 @@ export class SenderTests extends AITestClass {
disableXhr: true,
samplingPercentage: 90,
customHeaders: [{header: "header1",value:"value1"}],
alwaysUseXhrOverride: true
alwaysUseXhrOverride: true,
disableSendBeaconSplit: true
}
core.config.extensionConfig[id] = config;
this.clock.tick(1);
Expand All @@ -149,6 +151,7 @@ export class SenderTests extends AITestClass {
QUnit.assert.equal(90, curSenderConfig.samplingPercentage, "Channel samplingPercentage config is dynamically set");
QUnit.assert.deepEqual([{header: "header1",value:"value1"}], curSenderConfig.customHeaders, "Channel customHeaders config is dynamically set");
QUnit.assert.deepEqual(true, curSenderConfig.alwaysUseXhrOverride, "Channel alwaysUseXhrOverride config is dynamically set");
QUnit.assert.equal(true, curSenderConfig.disableSendBeaconSplit, "Channel disableSendBeaconSplit config is dynamically set");

core.config.extensionConfig[this._sender.identifier].emitLineDelimitedJson = undefined;
core.config.extensionConfig[this._sender.identifier].endpointUrl = undefined;
Expand Down Expand Up @@ -1151,6 +1154,270 @@ export class SenderTests extends AITestClass {
}
});

this.testCase({
name: "disableBeaconSplit is set to true, xhr should be used to send data diretly instead of splitting payloads.",
useFakeTimers: true,
test: () => {
let window = getWindow();
let fetchstub = this.sandbox.stub((window as any), "fetch");
let fakeXMLHttpRequest = (window as any).XMLHttpRequest;
let sessionStorage = window.sessionStorage;
QUnit.assert.ok(sessionStorage, "sessionStorage API is supported");
sessionStorage.clear();

let sendBeaconCalled = false;
this.hookSendBeacon((url: string) => {
sendBeaconCalled = true;
return false;
});

const sender = new Sender();
const cr = new AppInsightsCore();

sender.initialize({
instrumentationKey: "abc",
extensionConfig: {
[sender.identifier]: {
disableSendBeaconSplit: true,
onunloadDisableFetch: true,
disableXhr: true
// to make sure beacon is used
}
}

}, cr, []);
this.onDone(() => {
sender.teardown();
});

const telemetryItem: ITelemetryItem = {
name: "fake item",
iKey: "iKey",
baseType: "some type",
baseData: {
largePayload: new Array(64 + 1).join("test")
}
};

let buffer = sender._buffer;

QUnit.assert.ok(isBeaconApiSupported(), "Beacon API is supported");
QUnit.assert.equal(false, sendBeaconCalled, "Beacon API was not called before");
QUnit.assert.equal(0, this._getXhrRequests().length, "xhr sender was not called before");
QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should be clear");

try {
sender.processTelemetry(telemetryItem, null);
QUnit.assert.equal(1, buffer.getItems().length, "sender buffer should have one payload");
let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any);
QUnit.assert.equal(bufferItems.length, 1, "sender buffer should have one payload");
let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any);
QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload");
sender.onunloadFlush();
} catch(e) {
QUnit.assert.ok(false);
}

this.clock.tick(5);

QUnit.assert.equal(true, sendBeaconCalled, "Beacon API should be called");
QUnit.assert.equal(1, this._getXhrRequests().length, "xhr sender should be called");
let xhrRequest = this._getXhrRequests()[0];
QUnit.assert.equal(false, fetchstub.called, "fetch sender is not called");
QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should not have one payload");
QUnit.assert.equal(0, buffer.count(), "sender buffer should not have any payload");
let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any);
QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload");
let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any);
QUnit.assert.equal(1, sentItems.length, "sent buffer should have only one payload");

this.sendJsonResponse(xhrRequest, {}, 200);
bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any);
QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload");
sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any);
QUnit.assert.equal(0, sentItems.length, "sent buffer should have no payload");

(window as any).XMLHttpRequest = fakeXMLHttpRequest;
sessionStorage.clear();

}
});

this.testCase({
name: "disableBeaconSplit is set to false, xhr should not be called to send small payload.",
test: () => {
let window = getWindow();
let fetchstub = this.sandbox.stub((window as any), "fetch");
let fakeXMLHttpRequest = (window as any).XMLHttpRequest;
let sessionStorage = window.sessionStorage;
QUnit.assert.ok(sessionStorage, "sessionStorage API is supported");
sessionStorage.clear();

let sendBeaconCalled = 0;
this.hookSendBeacon((url: string) => {
sendBeaconCalled += 1;
return true;
});

const sender = new Sender();
const cr = new AppInsightsCore();

sender.initialize({
instrumentationKey: "abc",
extensionConfig: {
[sender.identifier]: {
onunloadDisableFetch: true,
disableXhr: true
// to make sure beacon is used
}
}

}, cr, []);
this.onDone(() => {
sender.teardown();
});

const telemetryItem: ITelemetryItem = {
name: "item",
iKey: "iKey",
baseType: "type",
baseData: {}
};


let buffer = sender._buffer;

QUnit.assert.ok(isBeaconApiSupported(), "Beacon API is supported");
QUnit.assert.equal(0, sendBeaconCalled, "Beacon API was not called before");
QUnit.assert.equal(0, this._getXhrRequests().length, "xhr sender was not called before");
QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should be clear");

try {
sender.processTelemetry(telemetryItem, null);
QUnit.assert.equal(1, buffer.getItems().length, "sender buffer should have one payload");
let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any);
QUnit.assert.equal(bufferItems.length, 1, "sender buffer should have one payload");
let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any);
QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload");
sender.onunloadFlush();
} catch(e) {
QUnit.assert.ok(false);
}

QUnit.assert.equal(1, sendBeaconCalled, "Beacon API should be called");
QUnit.assert.equal(0, this._getXhrRequests().length, "xhr sender should be called");
QUnit.assert.equal(false, fetchstub.called, "fetch sender is not called");
QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should not have one payload");
QUnit.assert.equal(0, buffer.count(), "sender buffer should not have any payload");
let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any);
QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload");
let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any);
QUnit.assert.equal(0, sentItems.length, "sent buffer should not have one payload");

(window as any).XMLHttpRequest = fakeXMLHttpRequest;
sessionStorage.clear();

}
});

this.testCase({
name: "disableBeaconSplit is set to false, xhr should be called to send large payload.",
useFakeTimers: true,
test: () => {
let window = getWindow();
let fetchstub = this.sandbox.stub((window as any), "fetch");
let fakeXMLHttpRequest = (window as any).XMLHttpRequest;
let sessionStorage = window.sessionStorage;
QUnit.assert.ok(sessionStorage, "sessionStorage API is supported");
sessionStorage.clear();

let sendBeaconCalled = 0;
this.hookSendBeacon((url: string) => {
sendBeaconCalled += 1;
if (sendBeaconCalled == 2) {
return true;
}
return false;
});

const sender = new Sender();
const cr = new AppInsightsCore();

sender.initialize({
instrumentationKey: "abc",
extensionConfig: {
[sender.identifier]: {
onunloadDisableFetch: true,
disableXhr: true
// to make sure beacon is used
}
}

}, cr, []);
this.onDone(() => {
sender.teardown();
});

const telemetryItem: ITelemetryItem = {
name: "item",
iKey: "iKey",
baseType: "type",
baseData: {}
};

const telemetryItem1: ITelemetryItem = {
name: "item",
iKey: "iKey1",
baseType: "type",
baseData: {}
};


let buffer = sender._buffer;

QUnit.assert.ok(isBeaconApiSupported(), "Beacon API is supported");
QUnit.assert.equal(0, sendBeaconCalled, "Beacon API was not called before");
QUnit.assert.equal(0, this._getXhrRequests().length, "xhr sender was not called before");
QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should be clear");

try {
sender.processTelemetry(telemetryItem, null);
sender.processTelemetry(telemetryItem1, null);
QUnit.assert.equal(2, buffer.getItems().length, "sender buffer should have one payload");
let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any);
QUnit.assert.equal(bufferItems.length, 2, "sender buffer should have one payload");
let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any);
QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload");
sender.onunloadFlush();
} catch(e) {
QUnit.assert.ok(false);
}
this.clock.tick(5);

QUnit.assert.equal(3, sendBeaconCalled, "Beacon API should be called 3 times");
QUnit.assert.equal(1, this._getXhrRequests().length, "xhr sender should be called");
let xhrRequest = this._getXhrRequests()[0];
QUnit.assert.equal(false, fetchstub.called, "fetch sender is not called");
QUnit.assert.equal(0, buffer.getItems().length, "sender buffer should not have one payload");
QUnit.assert.equal(0, buffer.count(), "sender buffer should not have any payload");
let bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any);
QUnit.assert.equal(bufferItems.length, 0, "sender buffer should be clear payload");
let sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any);
QUnit.assert.equal(1, sentItems.length, "sent buffer should have one payload");
QUnit.assert.ok(sentItems[0].indexOf("iKey1") >= 0, "sent buffer should have ikey1 payload");

this.sendJsonResponse(xhrRequest, {}, 200);
bufferItems = JSON.parse(sessionStorage.getItem("AI_buffer") as any);
QUnit.assert.equal(bufferItems.length, 0, "sender buffer should have no payload test1");
sentItems = JSON.parse(sessionStorage.getItem("AI_sentBuffer") as any);
QUnit.assert.equal(0, sentItems.length, "sent buffer should have zero payload test1");

(window as any).XMLHttpRequest = fakeXMLHttpRequest;
sessionStorage.clear();

}
});

this.testCase({
name: 'FetchAPI is used when isBeaconApiDisabled flag is true and disableXhr flag is true , use fetch sender.',
test: () => {
Expand Down
7 changes: 7 additions & 0 deletions channels/applicationinsights-channel-js/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ export interface ISenderConfig {
* @since 3.0.4
*/
alwaysUseXhrOverride?: boolean;

/**
* [Optional] Disable events splitting during sendbeacon.
* Default: false
* @since 3.0.6
*/
disableSendBeaconSplit?: boolean;
}

export interface IBackendResponse {
Expand Down
Loading

0 comments on commit f471839

Please sign in to comment.