Skip to content

STRATCONN-6637 [Hubspot] Fix duplicate ids and missing associations in Custom Object V2 batches - #3969

Open
monutwilio wants to merge 2 commits into
mainfrom
STRATCONN-6637/hs-dedupe
Open

STRATCONN-6637 [Hubspot] Fix duplicate ids and missing associations in Custom Object V2 batches#3969
monutwilio wants to merge 2 commits into
mainfrom
STRATCONN-6637/hs-dedupe

Conversation

@monutwilio

@monutwilio monutwilio commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Fixes STRATCONN-6637.

The HubSpot Custom Object V2 action sent one associated-record input per association payload. A mapping that associates the same record under two association labels — e.g. a contact linked to one company as both HUBSPOT_DEFINED:279 and HUBSPOT_DEFINED:1 — therefore repeated that company's id within a single POST /crm/v3/objects/company/batch/upsert:

{"inputs": [
  {"id": "6960031223", "idProperty": "ecs_object_id", "properties": {"ecs_object_id": "6960031223"}},
  {"id": "6960031223", "idProperty": "ecs_object_id", "properties": {"ecs_object_id": "6960031223"}}
]}

id_field_name must be a property with hasUniqueValue = true, so HubSpot rejects a batch that resolves two inputs to the same record. The failure is non-retryable and lands after the primary record upsert has already committed, so records synced while their associated records and associations were never created.

The label only matters to the subsequent /crm/v4/associations/.../batch/create call — it plays no part in the record upsert — but it was part of the de-duplication key, so both payloads survived into the request.

A second, independent defect sits in the same de-duplication pass: its key omitted the parent record, so two records associating to the same target under the same label collapsed into one and the later one silently lost its association. Both defects fire on every affected batch — the 400 above simply hides this one, and the staging run below reproduces it directly (monu5/monu6 synced together, only monu6 associated). Mappings with a single association label have been losing associations silently all along, with no 400 to signal it, tracked in STRATCONN-6969.

Changes

1. De-duplicate the request inputs, not the payload listfunctions/hubspot-association-functions.ts

readAssociatedRecords and upsertAssociatedRecords now build inputs from uniqueRecordsById(payloads), collapsing by id_field_value. The grouped payload list is untouched, so every label still produces its own association. Grouping guarantees object_type and id_field_name are constant within a group, so id_field_value alone is a sufficient key.

No downstream change was needed: returnAssociatedRecordsWithIds already maps each response result onto every payload in the group with a matching property value, rather than relying on index alignment with the request.

2. Add from_record_id to the association de-duplication keyfunctions/validation-functions.ts

deDuplicateAssociations keyed on object_type|association_label|id_field_name|id_field_value, omitting the parent record. Two records associating to the same target under the same label collapsed into one, and the second silently lost its association. This is a pre-existing data-loss bug, and change 1 would otherwise have masked it further.

The two changes depend on each other: change 2 alone puts more payloads back into the request, which is exactly what change 1 removes.

Not included

These were found while reviewing this change and are tracked separately rather than widening the diff:

  • STRATCONN-6963 — de-duplication is gated to upsert/update, so add sync mode is not de-duplicated at all.
  • STRATCONN-6964 — dissociations dropped on merge, not de-duplicated, and association/dissociation conflicts resolved by pipeline order rather than event timestamp.
  • STRATCONN-6965 — records may be silently dropped when HubSpot normalizes the id value (email). Needs verification before any fix.

Also considered and deliberately left alone: groupPayloads chunks at MAX_HUBSPOT_BATCH_SIZE before de-duplication runs, so a group larger than 100 payloads can still send one record id in two concurrent requests. The uniqueness constraint prevents duplicate records being created, so the worst case is a transient conflict on a record that does not yet exist, and the refactor needed to fix it is not worth carrying on this change.

Testing

__tests__/multi-label-associations.test.ts covers four cases, each asserting exact request bodies and nock.isDone():

  1. One record, two labels, one associated record → one input in the associated-record batch/upsert, two inputs in associations/batch/create.
  2. The same with association_sync_mode: read → one input in batch/read.
  3. Two records, one label, one shared associated record → both associations created (pins change 2).
  4. Two records × two labels each, one shared associated record → one record input, four association inputs (pins the interaction of both changes; this is the customer's production shape).

All four fail on the pre-fix code — 1, 2 and 4 on a duplicated record input, 3 on the missing second association.

  • Added unit tests for new functionality
  • Tested end-to-end using the local server
  • [If destination is already live] Tested for backward compatibility of destination. Note: New required fields are a breaking change.
  • [Segmenters] Tested in the staging environment
  • [Segmenters] [If applicable for this change] Tested for regression with Hadron.

Full hubspot suite passes: 25 suites, 169 tests, 90 snapshots. No snapshot updates were needed — the existing tests assert exact request bodies and none of them contained duplicate association ids. yarn lint and yarn typecheck clean.

No field definitions were added or changed, and no new required fields — existing subscriptions are unaffected apart from no longer sending duplicate ids.

Staging Testing

Before Fix

  • When associating to same object under different labels, it gives duplicate id in payload error.
Screenshot 2026-08-21 at 6 09 03 PM Screenshot 2026-08-21 at 6 09 24 PM
  • In case of performBatch, only one record gets associated to the object defined in association. I triggered event for monu5@test.com, monu6@test.com almost simultaneously and only monu6@test.com got the association.
Screenshot 2026-08-21 at 6 52 18 PM

After fix

  • When associating to same object under different labels, it succeeds.
Screenshot 2026-08-21 at 9 11 42 PM Screenshot 2026-08-21 at 9 11 55 PM
  • In case of performBatch, only one record gets associated to the object defined in association. I triggered event for monu7@test.com, monu8@test.com almost simultaneously and both got the association.
Screenshot 2026-08-21 at 9 20 43 PM

Security Review

  • Reviewed all field definitions for sensitive data (API keys, tokens, passwords, client secrets) and confirmed they use type: 'password' — no field definitions were changed by this PR.

🤖 Generated with Claude Code

… requests

Custom Object V2 sent one associated-record input per association payload.
A mapping that associates the same record under two association labels
therefore repeated that record's id within a single HubSpot batch, which
the id property's uniqueness constraint rejects with a 400. The contact
upsert had already committed by then, so records synced while their
companies and associations were never created.

Dedupe the request inputs by id_field_value in readAssociatedRecords and
upsertAssociatedRecords, leaving the grouped payload list untouched so
every label still gets its association. returnAssociatedRecordsWithIds
matches responses by property value rather than by index, so a request
carrying fewer inputs than its group still stamps record_id on all of them.

Also add from_record_id to the deDuplicateAssociations key. Without it,
two records associating to the same target under the same label collapsed
into one and the second silently lost its association - a latent data-loss
bug that the input dedupe above would otherwise have masked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 20, 2026 16:49
@monutwilio
monutwilio requested a review from a team as a code owner August 20, 2026 16:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes HubSpot batch failures and association data-loss by adjusting de-duplication behavior for associated-record upserts/reads and association creates.

Changes:

  • De-duplicate associated-record batch inputs by id_field_value (while keeping payloads intact so all labels still generate associations).
  • Fix association de-duplication to include the parent (from_record_id) in the key to prevent silently dropping associations across different “from” records.
  • Add unit tests covering multi-label and shared-associated-record scenarios (read + upsert modes).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/destination-actions/src/destinations/hubspot/upsertObject/functions/validation-functions.ts Updates association de-duplication key to include from_record_id.
packages/destination-actions/src/destinations/hubspot/upsertObject/functions/hubspot-association-functions.ts Adds uniqueRecordsById and uses it to de-dupe batch read/upsert inputs for associated records.
packages/destination-actions/src/destinations/hubspot/upsertObject/tests/multi-label-associations.test.ts Adds tests asserting request bodies for multi-label and shared associated-record cases.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI review requested due to automatic review settings August 21, 2026 05:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (2)

packages/destination-actions/src/destinations/hubspot/upsertObject/functions/hubspot-association-functions.ts:99

  • uniqueRecordsById currently overwrites the stored payload for duplicate keys, but Map iteration order remains based on the first insertion. That creates a subtle mismatch: the returned element order corresponds to first-seen IDs, while the payload object for that position corresponds to the last-seen duplicate. Since the record batch request only needs one representative per ID, make the behavior explicit by either (a) keeping the first payload for each id_field_value (do not overwrite if key already exists) or (b) if you intend to keep the last payload, also update insertion order (e.g., delete then set) so ordering and representative selection are consistent.
function uniqueRecordsById(payloads: AssociationPayload[]): AssociationPayload[] {
  const uniquePayloads = new Map<string, AssociationPayload>()

  for (const payload of payloads) {
    uniquePayloads.set(payload.object_details.id_field_value, payload)
  }

  return Array.from(uniquePayloads.values())
}

packages/destination-actions/src/destinations/hubspot/upsertObject/tests/multi-label-associations.test.ts:50

  • This beforeEach is synchronous but uses the done callback. In Jest, using done unnecessarily increases the chance of hangs if the callback is ever missed during edits. Consider removing done and returning nothing (or making it beforeEach(() => { ... })) to keep the setup idiomatic and safer.
beforeEach((done) => {
  testDestination = createTestIntegration(Definition)
  nock.cleanAll()
  done()
})

@monutwilio monutwilio changed the title STRATCONN-6637 [Hubspot] Fix duplicate ids in associated record batch requests STRATCONN-6637 [Hubspot] Fix duplicate ids and missing associations in Custom Object V2 batches Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants