Skip to content
Open
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
211 changes: 201 additions & 10 deletions spec/src/modules/tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2785,7 +2785,8 @@ describe('ConstructorIO - Tracker', () => {

expect(tracker.trackSearchResultsLoaded(term, {
...requiredParameters,
...optionalParameters }, userParameters)).to.equal(true);
...optionalParameters,
}, userParameters)).to.equal(true);
});

it('Should respond with a valid response when term, required parameters and user identifier are provided', (done) => {
Expand Down Expand Up @@ -4398,10 +4399,12 @@ describe('ConstructorIO - Tracker', () => {
apiKey: testApiKey,
fetch: fetchSpy,
});
const fullParameters = { ...requiredParameters,
const fullParameters = {
...requiredParameters,
type: 'add_to_loves',
displayName: 'Add To Loves List',
isCustomType: true };
isCustomType: true,
};

tracker.on('success', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);
Expand Down Expand Up @@ -4457,9 +4460,11 @@ describe('ConstructorIO - Tracker', () => {
apiKey: testApiKey,
fetch: fetchSpy,
});
const fullParameters = { ...requiredParameters,
const fullParameters = {
...requiredParameters,
display_name: 'Add To Loves List',
is_custom_type: true };
is_custom_type: true,
};

tracker.on('success', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);
Expand Down Expand Up @@ -4525,9 +4530,11 @@ describe('ConstructorIO - Tracker', () => {
apiKey: testApiKey,
fetch: fetchSpy,
});
const fullParameters = { ...requiredParameters,
const fullParameters = {
...requiredParameters,
type: 'add_to_loves',
is_custom_type: true };
is_custom_type: true,
};

tracker.on('error', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);
Expand Down Expand Up @@ -4694,7 +4701,7 @@ describe('ConstructorIO - Tracker', () => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);

try {
// Request
// Request
expect(fetchSpy).to.have.been.called;
expect(requestParams).to.have.property('key');
expect(requestParams).to.have.property('i');
Expand Down Expand Up @@ -4729,7 +4736,7 @@ describe('ConstructorIO - Tracker', () => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);

try {
// Request
// Request
expect(fetchSpy).to.have.been.called;
expect(requestParams).to.have.property('key');
expect(requestParams).to.have.property('i');
Expand Down Expand Up @@ -5772,6 +5779,98 @@ describe('ConstructorIO - Tracker', () => {

expect(tracker.trackRecommendationView(requiredParameters, { ...userParameters, userId })).to.equal(true);
});

it('Should return valid response and omit seed_item_ids if no seedItemIds provided', (done) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: There is no blank line between the previous it(...) block's closing }); and this new it(...) block. All other adjacent it(...) blocks in this file are separated by a blank line for readability. Add a blank line before each new it(...) block to match the surrounding style (this applies to both the trackRecommendationView and trackRecommendationClick test suites).

const { tracker } = new ConstructorIO({
apiKey: testApiKey,
fetch: fetchSpy,
});

tracker.on('success', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);

// Request
expect(fetchSpy).to.have.been.called;
expect(requestParams).to.not.have.property('seed_item_ids');

// Response
expect(responseParams).to.have.property('method').to.equal('POST');
expect(responseParams).to.have.property('message').to.equal('ok');

done();
});

expect(tracker.trackRecommendationView(requiredParameters, userParameters)).to.equal(true);
});
it('Should return valid response with seed_item_ids if seedItemIds is an array', (done) => {
const seedItemIds = ['123', '456'];
const { tracker } = new ConstructorIO({
apiKey: testApiKey,
fetch: fetchSpy,
});

tracker.on('success', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);

// Request
expect(fetchSpy).to.have.been.called;
expect(requestParams).to.have.property('seed_item_ids').to.deep.equal(seedItemIds);

// Response
expect(responseParams).to.have.property('method').to.equal('POST');
expect(responseParams).to.have.property('message').to.equal('ok');

done();
});

expect(tracker.trackRecommendationView({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true);
});
it('Should return valid response with seed_item_ids if seedItemIds is a string', (done) => {
const seedItemIds = '123';
const { tracker } = new ConstructorIO({
apiKey: testApiKey,
fetch: fetchSpy,
});

tracker.on('success', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);

// Request
expect(fetchSpy).to.have.been.called;
expect(requestParams).to.have.property('seed_item_ids').to.deep.equal([seedItemIds]);

// Response
expect(responseParams).to.have.property('method').to.equal('POST');
expect(responseParams).to.have.property('message').to.equal('ok');

done();
});

expect(tracker.trackRecommendationView({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true);
});
it('Should return valid response and omit seed_item_ids if seedItemIds is not string or array', (done) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important Issue: Missing test coverage for an edge case: an empty array (seedItemIds = []). The seedItemIds?.length guard in the implementation will silently drop an empty array (length is 0). There should be a test asserting that seed_item_ids is omitted when seedItemIds is [], to make this intentional behaviour explicit and prevent regressions. This applies to both trackRecommendationView and trackRecommendationClick.

const seedItemIds = { seedItemIds: ['123', '456'] };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The variable name seedItemIds is assigned an object { seedItemIds: ['123', '456'] } to represent an invalid type. This is confusing because the variable name implies it holds item IDs, but it's actually an object. A clearer name like invalidSeedItemIds or seedItemIdsObject would make the intent of the test immediately obvious without needing to read the value. This applies to both the trackRecommendationView and trackRecommendationClick versions of this test.

const { tracker } = new ConstructorIO({
apiKey: testApiKey,
fetch: fetchSpy,
});

tracker.on('success', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);

// Request
expect(fetchSpy).to.have.been.called;
expect(requestParams).to.not.have.property('seed_item_ids');

// Response
expect(responseParams).to.have.property('method').to.equal('POST');
expect(responseParams).to.have.property('message').to.equal('ok');

done();
});

expect(tracker.trackRecommendationView({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true);
});
});

describe('trackRecommendationClick', () => {
Expand Down Expand Up @@ -6341,6 +6440,98 @@ describe('ConstructorIO - Tracker', () => {

expect(tracker.trackRecommendationClick(requiredParameters, { ...userParameters, userId })).to.equal(true);
});

it('Should return valid response and omit seed_item_ids if no seedItemIds provided', (done) => {
Comment thread
constructor-claude-bedrock[bot] marked this conversation as resolved.
const { tracker } = new ConstructorIO({
apiKey: testApiKey,
fetch: fetchSpy,
});

tracker.on('success', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);

// Request
expect(fetchSpy).to.have.been.called;
expect(requestParams).to.not.have.property('seed_item_ids');

// Response
expect(responseParams).to.have.property('method').to.equal('POST');
expect(responseParams).to.have.property('message').to.equal('ok');

done();
});

expect(tracker.trackRecommendationClick(requiredParameters, userParameters)).to.equal(true);
});
it('Should return valid response with seed_item_ids if seedItemIds is an array', (done) => {
const seedItemIds = ['123', '456'];
const { tracker } = new ConstructorIO({
apiKey: testApiKey,
fetch: fetchSpy,
});

tracker.on('success', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);

// Request
expect(fetchSpy).to.have.been.called;
expect(requestParams).to.have.property('seed_item_ids').to.deep.equal(seedItemIds);

// Response
expect(responseParams).to.have.property('method').to.equal('POST');
expect(responseParams).to.have.property('message').to.equal('ok');

done();
});

expect(tracker.trackRecommendationClick({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true);
});
it('Should return valid response with seed_item_ids if seedItemIds is a string', (done) => {
const seedItemIds = '123';
const { tracker } = new ConstructorIO({
apiKey: testApiKey,
fetch: fetchSpy,
});

tracker.on('success', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);

// Request
expect(fetchSpy).to.have.been.called;
expect(requestParams).to.have.property('seed_item_ids').to.deep.equal([seedItemIds]);

// Response
expect(responseParams).to.have.property('method').to.equal('POST');
expect(responseParams).to.have.property('message').to.equal('ok');

done();
});

expect(tracker.trackRecommendationClick({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true);
});
it('Should return valid response and omit seed_item_ids if seedItemIds is not string or array', (done) => {
const seedItemIds = { seedItemIds: ['123', '456'] };
const { tracker } = new ConstructorIO({
apiKey: testApiKey,
fetch: fetchSpy,
});

tracker.on('success', (responseParams) => {
const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy);

// Request
expect(fetchSpy).to.have.been.called;
expect(requestParams).to.not.have.property('seed_item_ids');

// Response
expect(responseParams).to.have.property('method').to.equal('POST');
expect(responseParams).to.have.property('message').to.equal('ok');

done();
});

expect(tracker.trackRecommendationClick({ ...requiredParameters, seedItemIds }, userParameters)).to.equal(true);
});
});

describe('trackBrowseResultsLoaded', () => {
Expand Down Expand Up @@ -8095,7 +8286,7 @@ describe('ConstructorIO - Tracker', () => {
it('Should throw an error when providing no messageType parameter', () => {
const { tracker } = new ConstructorIO({ apiKey: testApiKey });

expect(tracker.on(null, () => {})).to.be.an('error');
expect(tracker.on(null, () => { })).to.be.an('error');
});

it('Should throw an error when providing an invalid callback parameter', () => {
Expand Down
24 changes: 24 additions & 0 deletions src/modules/tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,7 @@ class Tracker {
* @param {string} [parameters.resultId] - Recommendation result identifier (returned in response from Constructor)
* @param {string} [parameters.section="Products"] - Results section
* @param {object} [parameters.analyticsTags] - Pass additional analytics data
* @param {string[]|string} [parameters.seedItemIds] - Item ID(s) used to generate recommendations
* @param {object} userParameters - Parameters relevant to the user request
* @param {number} userParameters.sessionId - Session ID, utilized to personalize results
* @param {string} userParameters.clientId - Client ID, utilized to personalize results
Expand All @@ -1087,6 +1088,7 @@ class Tracker {
* url: 'https://demo.constructor.io/sandbox/farmstand',
* podId: '019927c2-f955-4020',
* numResultsViewed: 3,
* seedItemIds: ['UIH976']
* },
* {
* sessionId: 1,
Expand Down Expand Up @@ -1117,6 +1119,8 @@ class Tracker {
numResultsViewed = num_results_viewed,
items,
analyticsTags,
seed_item_ids,
seedItemIds = seed_item_ids,
} = parameters;

if (!helpers.isNil(resultCount)) {
Expand Down Expand Up @@ -1157,6 +1161,14 @@ class Tracker {
bodyParams.analytics_tags = analyticsTags;
}

if (seedItemIds?.length) {
if (typeof seedItemIds === 'string') {
bodyParams.seed_item_ids = [seedItemIds];
} else if (Array.isArray(seedItemIds)) {
bodyParams.seed_item_ids = seedItemIds;
}
}

const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`;
const requestMethod = 'POST';
const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod });
Expand Down Expand Up @@ -1193,6 +1205,7 @@ class Tracker {
* @param {number} [parameters.resultPositionOnPage] - Position of result on page
* @param {number} [parameters.numResultsPerPage] - Number of results on page
* @param {object} [parameters.analyticsTags] - Pass additional analytics data
* @param {string[]|string} [parameters.seedItemIds] - Item ID(s) used to generate recommendations
* @param {object} userParameters - Parameters relevant to the user request
Comment on lines 1206 to 1209
* @param {number} userParameters.sessionId - Session ID, utilized to personalize results
* @param {string} userParameters.clientId - Client ID, utilized to personalize results
Expand Down Expand Up @@ -1221,6 +1234,7 @@ class Tracker {
* podId: '019927c2-f955-4020',
* strategyId: 'complimentary',
* itemId: 'KMH876',
* seedItemIds: ['UIH976']
* },
* {
* sessionId: 1,
Expand Down Expand Up @@ -1259,6 +1273,8 @@ class Tracker {
item_name,
itemName = item_name,
analyticsTags,
seed_item_ids,
seedItemIds = seed_item_ids,
} = parameters;

if (variationId) {
Expand Down Expand Up @@ -1311,6 +1327,14 @@ class Tracker {
bodyParams.analytics_tags = analyticsTags;
}

if (seedItemIds?.length) {
if (typeof seedItemIds === 'string') {
bodyParams.seed_item_ids = [seedItemIds];
} else if (Array.isArray(seedItemIds)) {
bodyParams.seed_item_ids = seedItemIds;
}
}

const requestUrl = `${requestPath}${applyParamsAsString({}, userParameters, this.options)}`;
const requestMethod = 'POST';
const requestBody = applyParams(bodyParams, userParameters, { ...this.options, requestMethod });
Expand Down
4 changes: 3 additions & 1 deletion src/types/tracker.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EventEmitter } from 'events';
import { ConstructorClientOptions, NetworkParameters, ItemTracked, ItemTrackedPurchased } from '.';
import { ConstructorClientOptions, ItemTracked, ItemTrackedPurchased, NetworkParameters } from '.';

export default Tracker;

Expand Down Expand Up @@ -144,6 +144,7 @@ declare class Tracker {
resultId?: string;
section?: string;
analyticsTags?: Record<string, string>;
seedItemIds?: string[] | string;
},
userParameters?: TrackerUserParameters,
networkParameters?: NetworkParameters
Expand All @@ -163,6 +164,7 @@ declare class Tracker {
resultPositionOnPage?: number;
numResultsPerPage?: number;
analyticsTags?: Record<string, string>;
seedItemIds?: string[] | string;
},
userParameters?: TrackerUserParameters,
networkParameters?: NetworkParameters
Expand Down
Loading