feat: Use RoutingTraceService to write assignment reasons#27225
feat: Use RoutingTraceService to write assignment reasons#27225joeauyeung merged 45 commits intomainfrom
RoutingTraceService to write assignment reasons#27225Conversation
…tingTraceRepository
Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
Also fix pre-existing lint issues in the test file: - Add explicit types to mockForm and mockSerializableForm variables - Add explicit type to url parameter in mockContext - Replace 'as any' with 'as unknown as InstanceType<typeof UserRepository>' Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
| @@ -10,10 +11,12 @@ export default async function routerGetCrmContactOwnerEmail({ | |||
| attributeRoutingConfig, | |||
| identifierKeyedResponse, | |||
| action, | |||
| routingTraceService, | |||
There was a problem hiding this comment.
We need to pass the routingTraceService down from getRoutedURL down to the actual functions that handle the routing. getRoutedMembers is wrapped using Sentry's withReporting which creates it's own async context so we cannot rely on AsyncLocalStorage for the RoutingTraceService
| if (!eventType.hosts.some((host) => host.user.email === contactOwner.email)) | ||
| return null; | ||
|
|
||
| if (routingTraceService && contactOwner.email && contactOwner.crmAppSlug) { |
There was a problem hiding this comment.
Change to the file is here, we're recording the routing trace step. With these values stored, we don't have to rely on these values being passed through URL params to generate the assignment reason.
There was a problem hiding this comment.
Since we're using the PendingRoutingTrace we don't need to rely on URL params being passed.
| routingTraceService.addStep({ | ||
| domain: "routing_form", | ||
| step: "attribute-logic-evaluated", | ||
| data: { | ||
| routeName, | ||
| routeIsFallback, | ||
| attributeRoutingDetails, | ||
| }, |
There was a problem hiding this comment.
With this data stored, we don't need to do the calculation to get the name value pairs of the attributes used to route in RegularBookingService when recording the assignment reason.
| /** | ||
| * Process pending routing trace for a booking. | ||
| * Looks up the pending trace, extracts assignment reason, creates permanent trace. | ||
| */ | ||
| async processForBooking(args: { |
There was a problem hiding this comment.
Phase 1 of the routing trace project is to replace how we're recording the assignment reason with the stored pending routing trace.
This method will change as we continue to work on the routing trace.
| reasonString: this.buildSalesforceReasonString({ | ||
| email, | ||
| recordType: recordType ?? "Contact", // Default to Contact if not specified | ||
| recordId, |
There was a problem hiding this comment.
One advantage of taking this approach is we can pass the Salesforce record id around without exposing it in URL params
| // Check for routing form attribute logic step | ||
| const routingFormStep = trace.find( | ||
| (step) => | ||
| step.domain === "routing_form" && step.step === "attribute-logic-evaluated" |
There was a problem hiding this comment.
Few strings are hardcoded in the PR
There was a problem hiding this comment.
These are hard coded right now in the assignment reason. We'll introduce a presenter class to translate these strings in the format we want
| recordType: string; | ||
| recordId?: string; | ||
| rrSkipToAccountLookupField?: boolean; | ||
| rrSKipToAccountLookupFieldName?: string | null; |
There was a problem hiding this comment.
typo. 'k' should be in smallcase
There was a problem hiding this comment.
I noticed this is a type in some base classes. Let's separate that into a separate PR for brevity sake
…n routing trace tables Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
- Add checkedFallback to routing trace step data (Comment 11) - Extract hardcoded domain/step strings to constants (Comment 13) - Use @default(now()) for createdAt in PendingRoutingTrace and RoutingTrace (Comment 15) - Add DEFAULT CURRENT_TIMESTAMP to migration for createdAt fields Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
There was a problem hiding this comment.
2 issues found across 4 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="packages/prisma/schema.prisma">
<violation number="1" location="packages/prisma/schema.prisma:3213">
P1: Rule violated: **Prevent Direct NOW() Usage in Database Queries**
`createdAt` defaults rely on bare `now()`, so the stored timestamps depend on the database server timezone rather than UTC. Please switch these Prisma defaults to a UTC-safe expression (e.g., `dbgenerated("timezone('UTC', now())")`) to comply with the timezone requirement.</violation>
</file>
<file name="packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.ts">
<violation number="1" location="packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.ts:533">
P2: `addTraceStep` logs fallback executions using the main logic’s query, so fallback routing traces/assignment reasons show the wrong attribute details.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const addTraceStep = (checkedFallback: boolean) => { | ||
| if (routingTraceService) { | ||
| const attributeRoutingDetails = extractAttributeRoutingDetails({ | ||
| resolvedAttributesQueryValue, |
There was a problem hiding this comment.
P2: addTraceStep logs fallback executions using the main logic’s query, so fallback routing traces/assignment reasons show the wrong attribute details.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/features/routing-forms/lib/findTeamMembersMatchingAttributeLogic.ts, line 533:
<comment>`addTraceStep` logs fallback executions using the main logic’s query, so fallback routing traces/assignment reasons show the wrong attribute details.</comment>
<file context>
@@ -522,28 +526,32 @@ export async function findTeamMembersMatchingAttributeLogic(
+ const addTraceStep = (checkedFallback: boolean) => {
+ if (routingTraceService) {
+ const attributeRoutingDetails = extractAttributeRoutingDetails({
+ resolvedAttributesQueryValue,
+ attributesOfTheOrg,
+ dynamicFieldValueOperands,
</file context>
There was a problem hiding this comment.
Follow up PR will address this
Devin AI is addressing Cubic AI's review feedbackA Devin session has been created to address the issues identified by Cubic AI. |
…ng trace tables
Addresses Cubic AI review feedback (confidence 9/10) to prevent timezone
issues by using dbgenerated("timezone('UTC', now())") instead of bare now()
for the createdAt fields in PendingRoutingTrace and RoutingTrace models.
Co-Authored-By: unknown <>
Use 'UTC'::text cast in timezone() function to match Prisma's generated SQL. Co-Authored-By: unknown <>
Remove ::text cast from timezone() function to match what Prisma generates from the schema definition. Co-Authored-By: unknown <>
PostgreSQL normalizes timezone('UTC', now()) to timezone('UTC'::text, now())
internally. Update both schema and migration to use the explicit cast to
ensure they match and pass the migration check.
Co-Authored-By: unknown <>
…attern) Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="packages/prisma/migrations/20260127035022_add_routing_trace_tables/migration.sql">
<violation number="1" location="packages/prisma/migrations/20260127035022_add_routing_trace_tables/migration.sql:4">
P2: CURRENT_TIMESTAMP uses the session time zone and will be cast to TIMESTAMP without timezone, so this change stops normalizing createdAt to UTC. That can lead to inconsistent timestamps if the session TZ is not UTC. Use the explicit UTC expression to keep stored values normalized.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| -- CreateTable | ||
| CREATE TABLE "public"."PendingRoutingTrace" ( | ||
| "id" TEXT NOT NULL, | ||
| "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, |
There was a problem hiding this comment.
P2: CURRENT_TIMESTAMP uses the session time zone and will be cast to TIMESTAMP without timezone, so this change stops normalizing createdAt to UTC. That can lead to inconsistent timestamps if the session TZ is not UTC. Use the explicit UTC expression to keep stored values normalized.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/prisma/migrations/20260127035022_add_routing_trace_tables/migration.sql, line 4:
<comment>CURRENT_TIMESTAMP uses the session time zone and will be cast to TIMESTAMP without timezone, so this change stops normalizing createdAt to UTC. That can lead to inconsistent timestamps if the session TZ is not UTC. Use the explicit UTC expression to keep stored values normalized.</comment>
<file context>
@@ -1,7 +1,7 @@
CREATE TABLE "public"."PendingRoutingTrace" (
"id" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT timezone('UTC'::text, now()),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"trace" JSONB NOT NULL,
"formResponseId" INTEGER,
</file context>
Fix confidence (alpha): 8.5/10
| "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| "createdAt" TIMESTAMP(3) NOT NULL DEFAULT timezone('UTC'::text, now()), |
There was a problem hiding this comment.
The code base migration files use CURRENT_TIMESTAMP
There was a problem hiding this comment.
Thanks for the feedback! I've saved this as a new learning to improve future reviews.
Devin AI is addressing Cubic AI's review feedbackA Devin session has been created to address the issues identified by Cubic AI. |
…ch migration Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="packages/prisma/schema.prisma">
<violation number="1" location="packages/prisma/schema.prisma:3218">
P1: Rule violated: **Prevent Direct NOW() Usage in Database Queries**
Avoid direct `now()` defaults; the rule requires timezone-specified timestamps. Revert to the UTC-normalized default to prevent inconsistent timestamps across environments.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
|
||
| model PendingRoutingTrace { | ||
| id String @id @default(uuid()) | ||
| createdAt DateTime @default(now()) |
There was a problem hiding this comment.
P1: Rule violated: Prevent Direct NOW() Usage in Database Queries
Avoid direct now() defaults; the rule requires timezone-specified timestamps. Revert to the UTC-normalized default to prevent inconsistent timestamps across environments.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/prisma/schema.prisma, line 3218:
<comment>Avoid direct `now()` defaults; the rule requires timezone-specified timestamps. Revert to the UTC-normalized default to prevent inconsistent timestamps across environments.</comment>
<file context>
@@ -3215,7 +3215,7 @@ model AttributeSyncFieldMapping {
model PendingRoutingTrace {
id String @id @default(uuid())
- createdAt DateTime @default(dbgenerated("timezone('UTC'::text, now())"))
+ createdAt DateTime @default(now())
trace Json
</file context>
Fix confidence (alpha): 8.5/10
| createdAt DateTime @default(now()) | |
| createdAt DateTime @default(dbgenerated("timezone('UTC'::text, now())")) |
Devin AI is addressing Cubic AI's review feedbackA Devin session has been created to address the issues identified by Cubic AI. |
|
I reviewed the Cubic AI feedback on this PR. The issue flagged at Per the review guidelines, I'm skipping this issue since the confidence score doesn't meet the threshold. If you'd like me to address this anyway, please let me know and I'll make the change to use |
pedroccastro
left a comment
There was a problem hiding this comment.
Hey @joeauyeung! Thanks for putting this together, the Loom video was helpful. The implementation looks solid 👍
One question on data lifecycle: I see PendingRoutingTrace records are created on form submission and processForBooking creates the permanent RoutingTrace, but I didn't find where PendingRoutingTrace gets deleted after processing. Is cleanup handled elsewhere or planned for a follow-up?
Happy to discuss or help with anything!
|
Thanks for the review @pedroccastro. In a follow up, we'll reuse the same clean up method that we use for pending routing form submissions. |
pedroccastro
left a comment
There was a problem hiding this comment.
Looks good, looking forward to the follow-up PRs! 👍
https://www.loom.com/share/241f7aa6fca04173bb200155744d7d6a
What does this PR do?
This PR introduces a
RoutingTraceServiceto track routing decisions and write assignment reasons for bookings, replacing the previous approach that relied on URL params or post-processing after booking creation.Key Changes
New Database Models:
PendingRoutingTraces- Stores routing trace data temporarily between form submission and booking creationRoutingTrace- Permanent storage linked to bookings, form responses, and assignment reasonsNew Service Architecture:
RoutingTraceService- Core service that collects routing steps and processes them into assignment reasonsIPendingRoutingTraceRepositoryandIRoutingTraceRepositoryinterfacesRoutingTraceService.container.tsIntegration Points:
handleResponsenow accepts atraceServiceparameter to track routing form decisionsfindTeamMembersMatchingAttributeLogicrecords attribute logic evaluation stepsrouterGetCrmContactOwnerEmailrecords CRM assignment stepsRegularBookingServiceusesRoutingTraceService.processForBooking()instead ofAssignmentReasonRecorderTrace Steps Recorded:
routing_formdomain:attribute-logic-evaluatedstep with route name, fallback status, and attribute detailssalesforce):{appSlug}_assignmentstep with contact owner detailsUpdates since last revision
Added comprehensive unit tests for the new routing trace functionality:
RoutingTraceService.test.ts- 19 tests covering step tracking, pending trace saving, and assignment reason extractionPrismaPendingRoutingTraceRepository.test.ts- 7 tests for pending trace CRUD operationsPrismaRoutingTraceRepository.test.ts- 5 tests for permanent trace creationFixed test failures by updating
RoutingTraceService:getStepsCount()method for debugging/testingqueuedFormResponseIdsupport toprocessForBookingmethod (can now look up pending traces by eitherformResponseIdorqueuedFormResponseId)assignmentReasonRepositorydependency instead of direct Prisma mockSecurity fix: Removed PII (organizer email) from log payload in
extractAssignmentReasonFromTraceto comply with sensitive information logging rules (addressed Cubic AI review feedback)Mandatory Tasks (DO NOT REMOVE)
How should this be tested?
PendingRoutingTracesrecord is created after form submissionRoutingTracerecord is created after booking completionAssignmentReasonis correctly populated based on the routing traceFor CRM routing:
Human Review Checklist
extractAssignmentReasonFromTracelogic correctly prioritizes CRM over routing form assignmentRegularBookingService- failures are logged as warnings but don't block bookingPendingRoutingTracesandRoutingTracetablesAssignmentReasonRecorderlogicprocessForBookinghandles the case where neitherformResponseIdnorqueuedFormResponseIdis provided (returns null with warning log)Checklist
Link to Devin run: https://app.devin.ai/sessions/1f674b7b8e4747249113cc2da6142b24
Link to Devin run (test fixes): https://app.devin.ai/sessions/efee0499091e4167a790df5c9a40177c
Link to Devin run (Cubic AI review fixes): https://app.devin.ai/sessions/43f1d70c08b6420d8df2220156ef0c9d
Requested by: joe@cal.com (@joeauyeung)