Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
8 changes: 8 additions & 0 deletions .github/workflows/cross-browser-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
name: 'BrowserStack Test'
on: [push, pull_request, workflow_dispatch]

# Share a group across push and pull_request for the same branch so they
# do not run two BrowserStack matrices at once.
concurrency:
group: browserstack-test-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true

jobs:
browserstack-test:
name: 'BrowserStack Test'
runs-on: ubuntu-latest
# Cap hangs from half-open BrowserStack tunnels (GitHub default is 6h).
timeout-minutes: 30
steps:

- name: 'BrowserStack Env Setup' # Invokes the setup-env action
Expand Down
33 changes: 22 additions & 11 deletions src/cookieSyncManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,29 @@ export default function CookieSyncManager(
mpid: MPID,
cookieSyncDates: CookieSyncDates,
): void => {
const img = document.createElement('img');

mpInstance.Logger.verbose(InformationMessages.CookieSync);
img.onload = function() {
cookieSyncDates[moduleId] = new Date().getTime();

mpInstance._Persistence.saveUserCookieSyncDatesToPersistence(
mpid,
cookieSyncDates
try {
const img = document.createElement('img');

mpInstance.Logger.verbose(InformationMessages.CookieSync);
img.onload = function() {
cookieSyncDates[moduleId] = new Date().getTime();

mpInstance._Persistence.saveUserCookieSyncDatesToPersistence(
mpid,
cookieSyncDates
);
};
img.src = url;
} catch (error) {
// EdgeHTML throws on invalid img.src; contain so one bad pixel
// cannot break identify/setConsentState or later pixels.
mpInstance.Logger.error(
'Error performing cookie sync for module ID ' +
moduleId +
': ' +
((error as Error).message || error)
);
};
img.src = url;
}
};
}

Expand Down
19 changes: 18 additions & 1 deletion test/cross-browser-testing/browserstack.karma.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,16 @@ module.exports = function(config) {
config.set({
browserStack: {
username: process.env.BS_USERNAME,
accessKey: process.env.BS_ACCESS_KEY
accessKey: process.env.BS_ACCESS_KEY,
// Pin CI sessions to the workflow tunnel; locally karma still
// starts its own when BROWSERSTACK_LOCAL_IDENTIFIER is unset.
...(process.env.BROWSERSTACK_LOCAL_IDENTIFIER
? {
startTunnel: false,
localIdentifier:
process.env.BROWSERSTACK_LOCAL_IDENTIFIER,
}
: {}),
Comment thread
crisryantan marked this conversation as resolved.
Outdated
},
autoWatch: false,
customLaunchers,
Expand All @@ -85,6 +94,10 @@ module.exports = function(config) {
browserConsoleLogOptions,
client: {
captureConsole,
mocha: {
// Slow BrowserStack VMs need more than mocha's 2s default.
timeout: 20000,
},
},
junitReporter: {
outputDir: 'reports/',
Expand All @@ -95,6 +108,10 @@ module.exports = function(config) {
// 2000 ms (default) timeout
browserDisconnectTimeout: 50000,
browserDisconnectTolerance: 5,
// Session queueing often exceeds karma's 60s capture default.
captureTimeout: 300000,
// Slow VMs can stall longer than the 30s no-activity default.
browserNoActivityTimeout: 120000,
concurrency: 5,
});
};
49 changes: 49 additions & 0 deletions test/jest/cookieSyncManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,55 @@ describe('CookieSyncManager', () => {

expect(loggerSpy).toHaveBeenCalledWith('Performing cookie sync');
});

it('should log an error instead of throwing when the tracking pixel cannot be created', () => {
// EdgeHTML throws on invalid img.src; performCookieSync must
// contain that so identify/setConsentState do not throw.
const mockImage = {
onload: jest.fn(),
};
Object.defineProperty(mockImage, 'src', {
set() {
throw new Error('Invalid argument.');
},
});
jest.spyOn(document, 'createElement').mockReturnValue(
mockImage as unknown as HTMLImageElement
);

const errorSpy = jest.fn();
const saveSpy = jest.fn();

const mockMPInstance = ({
_Persistence: {
saveUserCookieSyncDatesToPersistence: saveSpy,
},
Logger: {
verbose: jest.fn(),
error: errorSpy,
},
} as unknown) as IMParticleWebSDKInstance;

const cookieSyncManager = new CookieSyncManager(mockMPInstance);

const cookieSyncDates: CookieSyncDates = {};
expect(() =>
cookieSyncManager.performCookieSync(
'https://test.com%3Fredirect%3Dhttps%3A%2F%2Fredirect.com',
42,
'1234',
cookieSyncDates,
)
).not.toThrow();

expect(errorSpy).toHaveBeenCalledWith(
'Error performing cookie sync for module ID 42: Invalid argument.'
);

// A failed pixel must not record a sync date
expect(cookieSyncDates[42]).toBeUndefined();
expect(saveSpy).not.toHaveBeenCalled();
});
});

describe('#isLastSyncDateExpired', () => {
Expand Down
3 changes: 2 additions & 1 deletion test/src/config/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,8 @@ var pluses = /\+/g,
},
waitForCondition = function async(
conditionFn,
timeout = 200,
// BrowserStack VMs need more than 200ms for mocked async work.
timeout = 3000,
interval = 10
) {
return new Promise((resolve, reject) => {
Expand Down
4 changes: 4 additions & 0 deletions test/src/tests-core-sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,10 @@ describe('core SDK', function() {
// fetching the config is async and we need to wait for it to finish
mParticle.getInstance()._Store.isInitialized.should.equal(true);

// Init's identify (mocked 400) must finish or the next identify
// is rejected as already in flight on slow browsers.
await waitForCondition(hasIdentityCallInflightReturned);

// have to manually call identify although it was called as part of init because we can only mock the server response once
fetchMockSuccess(urls.identify, {
mpid: 'MPID1',
Expand Down
101 changes: 55 additions & 46 deletions test/src/tests-runtimeToBatchEventsDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,56 +459,65 @@ describe('Old model to batch model conversion', () => {
expect(event.data.custom_event_type).to.equal('media')
});

it('Set width and height to 0 when window is defined but screen is not defined', () => {
const originalScreen = window.screen;
delete window.screen;

const sdkEvent: SDKEvent = {
EventName: "Pause Event",
EventCategory: 8,
ExpandedEventCount: 0,
EventDataType: 4,
EventAttributes: {
content_duration: '120000',
content_id: "1234567",
content_title: "My sweet sweet media",
content_type: "Video",
media_session_id: "07be2e14-7e05-4053-bcb5-94950365822d",
playhead_position: '7023.335999999999',
stream_type: "OnDemand",
},
ConsentState: null,
CurrencyCode: null,
CustomFlags: {},
DataPlan: {},
Debug: true,
DeviceId: "0edd580e-d887-44e4-89ae-cd65aa0ee933",
Location: null,
MPID: "-8433569646818451201",
OptOut: null,
SDKVersion: "2.11.15",
SourceMessageId: 'testSMID',
SessionId: "64102C03-592F-440D-8BCC-1D27AAA6B188",
SessionStartDate: 1603211322698,
Timestamp: 1603212299414,
ActiveTimeOnSite: 10,
UserAttributes: {},
UserIdentities: [],
IsFirstRun: true,
it('Set width and height to 0 when window is defined but screen is not defined', function(this: Mocha.Context) {
// EdgeHTML/IE: window.screen cannot be deleted; doing so poisons
// convertEvents for the rest of the run. Skip on those UAs only.
if (/(?:Edge|Trident)\//.test(window.navigator.userAgent)) {
this.skip();
}

const batch = Converter.convertEvents(
'-8433569646818451201',
[sdkEvent],
window.mParticle.getInstance()
);
const originalScreen = window.screen;

expect(batch).to.be.ok;
expect(batch.device_info.screen_height).to.equal(0);
expect(batch.device_info.screen_width).to.equal(0);
try {
delete window.screen;

const sdkEvent: SDKEvent = {
EventName: "Pause Event",
EventCategory: 8,
ExpandedEventCount: 0,
EventDataType: 4,
EventAttributes: {
content_duration: '120000',
content_id: "1234567",
content_title: "My sweet sweet media",
content_type: "Video",
media_session_id: "07be2e14-7e05-4053-bcb5-94950365822d",
playhead_position: '7023.335999999999',
stream_type: "OnDemand",
},
ConsentState: null,
CurrencyCode: null,
CustomFlags: {},
DataPlan: {},
Debug: true,
DeviceId: "0edd580e-d887-44e4-89ae-cd65aa0ee933",
Location: null,
MPID: "-8433569646818451201",
OptOut: null,
SDKVersion: "2.11.15",
SourceMessageId: 'testSMID',
SessionId: "64102C03-592F-440D-8BCC-1D27AAA6B188",
SessionStartDate: 1603211322698,
Timestamp: 1603212299414,
ActiveTimeOnSite: 10,
UserAttributes: {},
UserIdentities: [],
IsFirstRun: true,
}

const batch = Converter.convertEvents(
'-8433569646818451201',
[sdkEvent],
window.mParticle.getInstance()
);

// set screen back on
window.screen = originalScreen;
expect(batch).to.be.ok;
expect(batch.device_info.screen_height).to.equal(0);
expect(batch.device_info.screen_width).to.equal(0);
} finally {
// Restore even if an assertion fails so later suites are unaffected.
window.screen = originalScreen;
}
});

it('propagates PageUrl to page_url on the converted event', () => {
Expand Down
Loading