Skip to content

fix(recombee): Fix API inconsistencies#3767

Open
mstieranka wants to merge 5 commits into
segmentio:mainfrom
recombee:recombee-fix-timestamp-type
Open

fix(recombee): Fix API inconsistencies#3767
mstieranka wants to merge 5 commits into
segmentio:mainfrom
recombee:recombee-fix-timestamp-type

Conversation

@mstieranka
Copy link
Copy Markdown
Contributor

@mstieranka mstieranka commented Apr 29, 2026

Hi, we have identified several issues with out Recombee destination, and this pull request fixes all of them.

  1. In the actions Delete Cart Addition and Delete Bookmark, the request to our API was incorrectly setting the timestamp to milliseconds since epoch where seconds were expected. The following changes were made:
  • The tests now correctly expect a seconds-based timestamp, and the integration code has been updated to reflect this change.
  • In all actions, the optional field timestamp has been modified to use the datetime type instead of string, which more accurately reflects the intended values.
    • Note: When testing using the local server, the mapping accepts an arbitrary string field, however, when sending the payload, the current live version ends up returning HTTP 400 (the API rejects this parameter), whereas this version returns HTTP 500 (the validation already fails in Segment). I'm not sure if this is a breaking change, since both requests would have failed and the field itself is optional in all mappings. This is why the "Tested for backwards compatibility" task below is not yet marked as complete.
  • The timestamp handling has generally been made more robust to accept any form of date string parseable by new Date(str) or a number representing either seconds or milliseconds since epoch. The field description has been changed to reflect this fact.
  1. The default timestamp used by Segment ($.timestamp) is corrected for clock skew, however, that means that the Recombee API receives a different timestamp than the user sent. Since the Delete Bookmark action expects the exact same timestamp as when sending the request, this meant that such a use case wouldn't work. The following changes were made:
  • The timestamp mapping in Add* events has been changed to use $.properties.timestamp by default, falling back to $.timestamp if not present.
  • The timestamp mapping in Delete* events now also uses $.properties.timestamp.
  • The field descriptions were updated to clearly state this fact and advise users.
  1. In Segment Ecommerce events, the price field is stated per-unit, however, the Recombee API expects the price and profit fields to be stated per the given quantity. Therefore, the destination now multiplies price by amount to match the expectation of the Recombee API.

Testing

Include any additional information about the testing you have completed to
ensure your changes behave as expected. For a speedy review, please check
any of the tasks you completed below during your testing.

  • 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.

Security Review

Please ensure sensitive data is properly protected in your integration.

  • Reviewed all field definitions for sensitive data (API keys, tokens, passwords, client secrets) and confirmed they use type: 'password'

New Destination Checklist

  • Extracted all action API versions to verioning-info.ts file. example

Copilot AI review requested due to automatic review settings April 29, 2026 15:37
@mstieranka mstieranka requested a review from a team as a code owner April 29, 2026 15:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes timestamp handling for the Recombee destination, ensuring delete interactions send timestamps in seconds (as required by Recombee) and aligning action field schemas to use the datetime field type.

Changes:

  • Update Delete Bookmark / Delete Cart Addition URL construction to send timestamp as epoch seconds (converting from ms/ISO when needed).
  • Change timestamp input fields across Recombee actions from string to datetime (and update generated types accordingly).
  • Update unit tests/snapshots to reflect the new timestamp expectations and payload shapes.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/destination-actions/src/destinations/recombee/setViewPortionFromWatchTime/index.ts Switch timestamp field to datetime.
packages/destination-actions/src/destinations/recombee/setViewPortionFromWatchTime/generated-types.ts Update timestamp payload type to string | number.
packages/destination-actions/src/destinations/recombee/setViewPortionFromWatchTime/tests/snapshots/snapshot.test.ts.snap Snapshot update for timestamp value shape.
packages/destination-actions/src/destinations/recombee/setViewPortion/index.ts Switch timestamp field to datetime.
packages/destination-actions/src/destinations/recombee/setViewPortion/generated-types.ts Update timestamp payload type to string | number.
packages/destination-actions/src/destinations/recombee/setViewPortion/tests/snapshots/snapshot.test.ts.snap Snapshot update for timestamp value shape.
packages/destination-actions/src/destinations/recombee/recombeeApiClient.ts Convert delete timestamp to epoch seconds and build query via URLSearchParams.
packages/destination-actions/src/destinations/recombee/deleteCartAddition/index.ts Switch timestamp field to datetime.
packages/destination-actions/src/destinations/recombee/deleteCartAddition/generated-types.ts Update timestamp payload type to string | number.
packages/destination-actions/src/destinations/recombee/deleteCartAddition/tests/index.test.ts Update tests to expect 10-digit (seconds) timestamps and seconds-based URL assertions.
packages/destination-actions/src/destinations/recombee/deleteCartAddition/tests/snapshots/snapshot.test.ts.snap Snapshot update for timestamp value shape.
packages/destination-actions/src/destinations/recombee/deleteBookmark/index.ts Switch timestamp field to datetime.
packages/destination-actions/src/destinations/recombee/deleteBookmark/generated-types.ts Update timestamp payload type to string | number.
packages/destination-actions/src/destinations/recombee/deleteBookmark/tests/index.test.ts Update tests to expect 10-digit (seconds) timestamps and seconds-based URL assertions.
packages/destination-actions/src/destinations/recombee/deleteBookmark/tests/snapshots/snapshot.test.ts.snap Snapshot update for timestamp value shape.
packages/destination-actions/src/destinations/recombee/addRating/index.ts Switch timestamp field to datetime.
packages/destination-actions/src/destinations/recombee/addRating/generated-types.ts Update timestamp payload type to string | number.
packages/destination-actions/src/destinations/recombee/addRating/tests/snapshots/snapshot.test.ts.snap Snapshot update for timestamp value shape.
packages/destination-actions/src/destinations/recombee/addPurchase/index.ts Switch timestamp field to datetime.
packages/destination-actions/src/destinations/recombee/addPurchase/generated-types.ts Update timestamp payload type to string | number.
packages/destination-actions/src/destinations/recombee/addPurchase/tests/snapshots/snapshot.test.ts.snap Snapshot update for timestamp value shape.
packages/destination-actions/src/destinations/recombee/addDetailView/index.ts Switch timestamp field to datetime.
packages/destination-actions/src/destinations/recombee/addDetailView/generated-types.ts Update timestamp payload type to string | number.
packages/destination-actions/src/destinations/recombee/addDetailView/tests/snapshots/snapshot.test.ts.snap Snapshot update for timestamp value shape.
packages/destination-actions/src/destinations/recombee/addCartAddition/index.ts Switch timestamp field to datetime.
packages/destination-actions/src/destinations/recombee/addCartAddition/generated-types.ts Update timestamp payload type to string | number.
packages/destination-actions/src/destinations/recombee/addCartAddition/tests/snapshots/snapshot.test.ts.snap Snapshot update for timestamp value shape.
packages/destination-actions/src/destinations/recombee/addBookmark/index.ts Switch timestamp field to datetime.
packages/destination-actions/src/destinations/recombee/addBookmark/generated-types.ts Update timestamp payload type to string | number.
packages/destination-actions/src/destinations/recombee/addBookmark/tests/snapshots/snapshot.test.ts.snap Snapshot update for timestamp value shape.
packages/destination-actions/src/destinations/recombee/tests/snapshots/snapshot.test.ts.snap Update destination-level snapshots for timestamp value shape across actions.

Comment thread packages/destination-actions/src/destinations/recombee/recombeeApiClient.ts Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 30, 2026 13:17
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes Recombee delete-interaction timestamp handling (seconds vs. milliseconds) and updates Recombee action field schemas so timestamp is modeled as a datetime (and thus typed as string | number) across the destination.

Changes:

  • Fix Delete Cart Addition and Delete Bookmark URL timestamp to send Unix seconds (and add more robust timestamp parsing/normalization for deletes).
  • Change timestamp input field type from stringdatetime across Recombee actions and update generated payload types accordingly.
  • Update unit tests and snapshots to reflect the datetime field behavior and the corrected delete timestamp semantics.

Reviewed changes

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

Show a summary per file
File Description
packages/destination-actions/src/destinations/recombee/recombeeApiClient.ts Normalize delete URL query params via URLSearchParams; convert/delete timestamp to epoch seconds and validate invalid timestamps.
packages/destination-actions/src/destinations/recombee/deleteCartAddition/index.ts Change timestamp field type to datetime.
packages/destination-actions/src/destinations/recombee/deleteCartAddition/generated-types.ts Update payload timestamp type to string | number.
packages/destination-actions/src/destinations/recombee/deleteCartAddition/tests/index.test.ts Update delete URL expectations to 10-digit seconds timestamps.
packages/destination-actions/src/destinations/recombee/deleteCartAddition/tests/snapshots/snapshot.test.ts.snap Snapshot update for datetime timestamp example.
packages/destination-actions/src/destinations/recombee/deleteBookmark/index.ts Change timestamp field type to datetime.
packages/destination-actions/src/destinations/recombee/deleteBookmark/generated-types.ts Update payload timestamp type to string | number.
packages/destination-actions/src/destinations/recombee/deleteBookmark/tests/index.test.ts Update delete URL expectations to 10-digit seconds timestamps.
packages/destination-actions/src/destinations/recombee/deleteBookmark/tests/snapshots/snapshot.test.ts.snap Snapshot update for datetime timestamp example.
packages/destination-actions/src/destinations/recombee/addBookmark/index.ts Change timestamp field type to datetime.
packages/destination-actions/src/destinations/recombee/addBookmark/generated-types.ts Update payload timestamp type to string | number.
packages/destination-actions/src/destinations/recombee/addBookmark/tests/snapshots/snapshot.test.ts.snap Snapshot update for datetime timestamp example.
packages/destination-actions/src/destinations/recombee/addCartAddition/index.ts Change timestamp field type to datetime.
packages/destination-actions/src/destinations/recombee/addCartAddition/generated-types.ts Update payload timestamp type to string | number.
packages/destination-actions/src/destinations/recombee/addCartAddition/tests/snapshots/snapshot.test.ts.snap Snapshot update for datetime timestamp example.
packages/destination-actions/src/destinations/recombee/addDetailView/index.ts Change timestamp field type to datetime.
packages/destination-actions/src/destinations/recombee/addDetailView/generated-types.ts Update payload timestamp type to string | number.
packages/destination-actions/src/destinations/recombee/addDetailView/tests/snapshots/snapshot.test.ts.snap Snapshot update for datetime timestamp example.
packages/destination-actions/src/destinations/recombee/addPurchase/index.ts Change timestamp field type to datetime.
packages/destination-actions/src/destinations/recombee/addPurchase/generated-types.ts Update payload timestamp type to string | number.
packages/destination-actions/src/destinations/recombee/addPurchase/tests/snapshots/snapshot.test.ts.snap Snapshot update for datetime timestamp example.
packages/destination-actions/src/destinations/recombee/addRating/index.ts Change timestamp field type to datetime.
packages/destination-actions/src/destinations/recombee/addRating/generated-types.ts Update payload timestamp type to string | number.
packages/destination-actions/src/destinations/recombee/addRating/tests/snapshots/snapshot.test.ts.snap Snapshot update for datetime timestamp example.
packages/destination-actions/src/destinations/recombee/setViewPortion/index.ts Change timestamp field type to datetime.
packages/destination-actions/src/destinations/recombee/setViewPortion/generated-types.ts Update payload timestamp type to string | number.
packages/destination-actions/src/destinations/recombee/setViewPortion/tests/snapshots/snapshot.test.ts.snap Snapshot update for datetime timestamp example.
packages/destination-actions/src/destinations/recombee/setViewPortionFromWatchTime/index.ts Change timestamp field type to datetime.
packages/destination-actions/src/destinations/recombee/setViewPortionFromWatchTime/generated-types.ts Update payload timestamp type to string | number.
packages/destination-actions/src/destinations/recombee/setViewPortionFromWatchTime/tests/snapshots/snapshot.test.ts.snap Snapshot update for datetime timestamp example.
packages/destination-actions/src/destinations/recombee/tests/snapshots/snapshot.test.ts.snap Aggregate snapshot updates for datetime timestamp examples across actions.

Comment thread packages/destination-actions/src/destinations/recombee/recombeeApiClient.ts Outdated
Comment thread packages/destination-actions/src/destinations/recombee/recombeeApiClient.ts Outdated
@mstieranka mstieranka marked this pull request as draft May 11, 2026 09:01
@joe-ayoub-segment
Copy link
Copy Markdown
Contributor

Hi @mstieranka please ping me when this PR is ready for review.

Copilot AI review requested due to automatic review settings May 12, 2026 10:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 39 out of 39 changed files in this pull request and generated 2 comments.

Comment on lines +97 to +101
// 10,000,000,000 is the year 2286 in seconds, anything larger is likely in milliseconds
if (numValue >= 10000000000) {
return numValue / 1000
} else {
return numValue
Comment on lines +159 to 170
function getDeleteUrl(interactionType: string, params: DeleteParams): string {
const query = new URLSearchParams({
userId: params.userId,
itemId: params.itemId
})

if (params.timestamp !== undefined && params.timestamp !== null) {
query.append('timestamp', String(datetimeToEpochSeconds(params.timestamp)))
}
return url

return `/${interactionType}/?${query.toString()}`
}
@mstieranka mstieranka changed the title fix(recombee): Send time as seconds in Delete events, change field type fix(recombee): Fix API inconsistencies May 12, 2026
@mstieranka mstieranka force-pushed the recombee-fix-timestamp-type branch from 61b9364 to 9b20518 Compare May 12, 2026 10:18
@mstieranka mstieranka marked this pull request as ready for review May 12, 2026 10:30
Copilot AI review requested due to automatic review settings May 12, 2026 10:30
@mstieranka
Copy link
Copy Markdown
Contributor Author

Hi @joe-ayoub-segment , this PR has had some changes added to it and is now ready for review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 39 out of 39 changed files in this pull request and generated 1 comment.

Comment on lines +97 to +101
// 10,000,000,000 is the year 2286 in seconds, anything larger is likely in milliseconds
if (numValue >= 10000000000) {
return numValue / 1000
} else {
return numValue
Copy link
Copy Markdown
Contributor

@joe-ayoub-segment joe-ayoub-segment left a comment

Choose a reason for hiding this comment

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

Hi @mstieranka thanks for the PR and apologies for the delay reviewing it.

It's generally good - however some important points I left comments on.

If you can respond to those, and potentially fix, that would be great.

Thanks,
Joe

})

if (params.timestamp !== undefined && params.timestamp !== null) {
query.append('timestamp', String(datetimeToEpochSeconds(params.timestamp)))
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.

consider calling datetimeToEpochSeconds into a variable first, then check if it is a valid timestamp before doing query.append.

}
}

function datetimeToEpochSeconds(datetime: string | number): number {
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.

The millisecond detection heuristic is fragile. The threshold 10000000000 (Nov 2286 in seconds) means any epoch-seconds value after that date would be incorrectly halved. Conversely, millisecond timestamps before Sept 9, 2001 (10000000000 ms = ~115 days from
epoch) would be treated as seconds. Both are unlikely in practice but the magic number deserves at least a comment explaining the tradeoff.

The division produces fractional seconds. numValue / 1000 for an odd millisecond value yields something like 1716048000.123. If the consumer expects an integer epoch (as the name "EpochSeconds" suggests), this should be Math.floor(numValue / 1000).

String(datetime).trim() !== '' is dead code for the number input type. Number('') is 0 which would pass !isNaN, so the check guards against empty strings — fine. But String(someNumber).trim() !== '' is always true for any number input. Not wrong, just
unnecessary for that branch.

new Date(datetime) when datetime is a number that somehow failed the first check — this can't actually happen. If datetime is a number, Number(datetime) will never be NaN. So the new Date(datetime) path only runs for non-numeric strings, which is correct but not
obvious from reading the flow.

No timezone consideration for string parsing. new Date("2024-01-15") vs new Date("2024-01-15T00:00:00") behave differently (UTC vs local time) depending on the format. If callers pass date-only strings, the result will vary by environment.

The biggest practical issue is the missing Math.floor — everything else is edge-case or readability.

Comment on lines +47 to +53
default: {
'@if': {
exists: { '@path': '$.properties.timestamp' },
then: { '@path': '$.properties.timestamp' },
else: { '@path': '$.timestamp' }
}
},
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.

Unless you have a good reason, I would leave the detault as $.timestamp as that's the location where Segment event timestamps are always found.

}

function datetimeToEpochSeconds(datetime: string | number): number {
const numValue = Number(datetime)
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.

If user passes "" this will evaluate to 0. It might enter a code path you didn't anticipate below. Please check.

) => ({
label: 'Timestamp',
description: `The UTC timestamp of the ${interactionName} to delete, in Unix seconds, Unix milliseconds, or ISO-8601 format. Must match the timestamp used in the ${interactionName} to be deleted. If omitted, all ${interactionName}s for the given \`userId\` and \`itemId\` are deleted.`,
type: 'datetime',
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.

Note the original field you are now generating via a function originally was not of type: 'datetime'.
Adding type: 'datetime' might break data collection for customers who are already sending in non datetime string to this field.

If you confirm that this is OK and deliberate that's OK.

) => ({
label: 'Timestamp',
description: `The UTC timestamp of when the ${interactionName} occurred, in Unix seconds, Unix milliseconds, or ISO-8601 format. When recording interactions you plan to later delete by exact timestamp — whether via this destination or the Recombee API directly — avoid mapping the root \`timestamp\` here, as it may be corrected for clock skew. Use \`properties.timestamp\` instead.`,
type: 'datetime',
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.

Note the original field you are now generating via a function originally was not of type: 'datetime'.
Adding type: 'datetime' might break data collection for customers who are already sending in non datetime string to this field.

If you confirm that this is OK and deliberate that's OK.

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