[CDX-470] Add additionalTrackingKeys option - #472
Conversation
…vents Adds a new `additionalTrackingKeys` client option that duplicates tracking events to additional API keys. When configured, each tracking event queued via RequestQueue is also sent to each additional key with the `key` parameter swapped in both the URL and POST body.
There was a problem hiding this comment.
Pull request overview
Adds an additionalTrackingKeys client option to support duplicating tracking events across multiple Constructor.io API keys, primarily by cloning queued RequestQueue entries with the key swapped in the URL/body.
Changes:
- Introduces
additionalTrackingKeys?: string[]onConstructorClientOptionsand wires it throughConstructorIOoptions. - Updates
RequestQueueto enqueue duplicate tracking requests per additional key. - Adds unit + integration tests validating duplication behavior and invalid-key filtering.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/request-queue.js | Duplicates queued tracking requests for each configured additional key |
| src/types/index.d.ts | Adds additionalTrackingKeys?: string[] to the public options type |
| src/constructorio.js | Plumbs additionalTrackingKeys through normalized client options |
| spec/src/utils/request-queue.js | Unit tests for duplication, empty/missing option, invalid entries, GET behavior |
| spec/src/modules/tracker.js | Integration test asserting fetch is called for both primary + additional keys |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
esezen
left a comment
There was a problem hiding this comment.
Thanks! This is looking great. I left some comments
- Use Set to remove duplicate additionalTrackingKeys and exclude the primary apiKey - Assert duplicate request URL does not contain the original key - Add test verifying request bodies are identical except for the key field - Improve JSDoc description for additionalTrackingKeys parameter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-client-js Resolve conflicts: keep both useWindowParameters (from master) and additionalTrackingKeys (from this branch) in constructorio.js and index.d.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jjl014
left a comment
There was a problem hiding this comment.
Looking pretty solid to me. I did leave a couple comments that I'd like to get your thoughts on. Thanks!
| const encodedOriginalKey = helpers.encodeURIComponentRFC3986(this.options.apiKey); | ||
|
|
||
| const uniqueKeys = new Set( | ||
| additionalKeys.filter((key) => key && typeof key === 'string' && key !== this.options.apiKey), | ||
| ); | ||
|
|
||
| uniqueKeys.forEach((additionalKey) => { | ||
| const encodedAdditionalKey = helpers.encodeURIComponentRFC3986(additionalKey); | ||
| const swappedUrl = url.replace(`key=${encodedOriginalKey}`, `key=${encodedAdditionalKey}`); |
There was a problem hiding this comment.
Any reason why we're doing this validation and uniqueness check every time we queue an event?
It feels like an additional step that could be handled earlier (e.g. during client instantiation in the construtor or when setClientOptions is called)?
There was a problem hiding this comment.
Thanks @jjl014 This is a good consideration!
We can definitely handle the sanitization check earlier in the system and save the time to conduct the check every time we queue an event 👍
| this.options.serviceUrl = formattedServiceUrl || this.options.serviceUrl; | ||
| } | ||
|
|
||
| if (Array.isArray(additionalTrackingKeys)) { |
There was a problem hiding this comment.
Not a big deal, but if we wanted to stop sending events to multiple keys, we would have to send [] at the moment. Should also support null here?
There was a problem hiding this comment.
Yes I think this is a good point!
We should allow users to stop sending events to additional keys by passing null to additionalTrackingKeys , instead of forcing them to send an empty array []
|
Hi @jjl014
|
jjl014
left a comment
There was a problem hiding this comment.
Thanks for making the updates! The changes are looking good from what I can tell, but I noticed that some of the newly added tests and a variety of tests from other parts of the spec are failing.
I'm guessing the ones failing on master might be expected. I think @Mudaafi mentioned he was working on fixing them. However, I wouldn't expect the tests added from this PR to fail as well. 🤔
…naltrackingkeys-option-to-client-js
Mudaafi
left a comment
There was a problem hiding this comment.
Looks good, just a couple of comments
|
|
||
| // Assert an option on the client and every sub-module that shares its `options` reference. | ||
| // Uses `deep.equal` to enforce strict structural equality for objects and arrays. | ||
| const expectSharedOption = (instance, option, expected) => { |
There was a problem hiding this comment.
| const expectSharedOption = (instance, option, expected) => { | |
| const expectSharedOption = (instance, key, expectedValue) => { |
- Rename expectSharedOption params for clarity (option -> key, expected -> expectedValue) - Add guard in tracker tests to fail if fetch is called more than expected
|
|
||
| // Assert an option on the client and every sub-module that shares its `options` reference. | ||
| // Uses `deep.equal` to enforce strict structural equality for objects and arrays. | ||
| const expectSharedOption = (instance, key, expectedValue) => { |
There was a problem hiding this comment.
Suggestion: The new expectSharedOption helper always uses .deep.equal, which silently changed the assertion semantics for the refactored tests that previously used .equal (strict reference equality) for scalar options such as apiKey, userId, sessionId, and serviceUrl. For primitives, .deep.equal and .equal behave identically, so there is no functional regression. However, the serviceUrl test (line ~698) previously only asserted on the sub-module options (not instance.options) before setClientOptions, and expectSharedOption now also asserts instance.options. Verify that instance.options.serviceUrl is intentionally set to 'https://ac.cnstrc.com' at construction time before asserting — if it is, the change is fine. If not, the test now passes for an unintended reason.
| additionalTrackingKeys: [additionalKey], | ||
| }); | ||
|
|
||
| let callCount = 0; |
There was a problem hiding this comment.
Suggestion: Both integration tests in the additionalTrackingKeys describe block use a manual callCount counter with a checkComplete callback to detect the 2nd call. Since done(new Error(...)) is called when callCount > 2 but the test doesn't explicitly stop further success/error events from firing, the done callback could theoretically be called multiple times if the event emitter fires more events, which Mocha treats as an error. Consider using a stub or once listener instead:
tracker.once('success', () => {
// first call — no assertions yet
tracker.once('success', () => {
expect(fetchSpy).to.have.been.calledTwice;
// ...assertions...
done();
});
});Alternatively, wrapping in Promise.all with two sinon stubs would be cleaner and remove the timing fragility.
Mudaafi
left a comment
There was a problem hiding this comment.
Thanks for making the changes
Summary
additionalTrackingKeysoption toConstructorClientOptionsthat accepts an array of API key stringsRequestQueueis duplicated for each additional key, with thekeyswapped in both the URL query string and POST bodyUsage
Changes
src/types/index.d.ts— AddedadditionalTrackingKeys?: string[]to options interfacesrc/constructorio.js— Pass through the new optionsrc/utils/request-queue.js— Duplicate queued requests for each additional keyspec/src/utils/request-queue.js— Unit tests (5 cases)spec/src/modules/tracker.js— Integration testTest plan
Resolves CDX-470