Add VoiceOps name mapping fields - #3870
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds explicit name-mapping fields and an optional “split agent names from email” fallback for the VoiceOps Send Call Completed action, so customers can control customer/agent naming and fill missing agent names when upstream events only contain emails.
Changes:
- Introduces
customer_*andagent_*mapping fields plusassign_first_last_name_by_splitting_email. - Implements derivation of missing agent + agent-leg names from email local-part before validation/request.
- Updates generated payload types and expands Jest coverage + snapshots for the new behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/destination-actions/src/destinations/voiceops/sendCallCompleted/index.ts | Adds new mapping fields, email-splitting name derivation, and uses derived payload during perform. |
| packages/destination-actions/src/destinations/voiceops/sendCallCompleted/generated-types.ts | Updates Payload typings to include new fields and makes agent-leg names optional. |
| packages/destination-actions/src/destinations/voiceops/sendCallCompleted/tests/index.test.ts | Adds tests for name forwarding and email-derived names; updates prior test naming/expectations. |
| packages/destination-actions/src/destinations/voiceops/sendCallCompleted/tests/snapshots/snapshot.test.ts.snap | Updates action snapshot to reflect new fields. |
| packages/destination-actions/src/destinations/voiceops/tests/snapshots/snapshot.test.ts.snap | Updates destination snapshot to reflect new fields. |
| return { | ||
| first_name: titleCase(nameParts[0]), | ||
| last_name: nameParts.length > 1 ? nameParts.slice(1).map(titleCase).join(' ') : undefined | ||
| } |
| first_name: { | ||
| label: 'First Name', | ||
| description: 'The first name of the agent for this call leg.', | ||
| type: 'string', | ||
| required: true | ||
| type: 'string' | ||
| }, | ||
| last_name: { | ||
| label: 'Last Name', | ||
| description: 'The last name of the agent for this call leg.', | ||
| type: 'string', | ||
| required: true | ||
| type: 'string' | ||
| } |
798e726 to
5362ee8
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
packages/destination-actions/src/destinations/voiceops/sendCallCompleted/index.ts:84
- With the new
assign_first_last_name_by_splitting_emailfeature, these validation errors can become less actionable when names are omitted intentionally (expecting derivation) but derivation fails (e.g., missing/invalidagent_emailon a leg). Consider updating the error text to mention the derivation option and the requirement for a validagent_emailwhen names are not provided (e.g., 'Provide agentLegs.first_name or enable email splitting and supply agentLegs.agent_email').
for (const agentLeg of payload.agentLegs ?? []) {
if (!agentLeg.first_name?.trim()) {
throw new PayloadValidationError('agentLegs.first_name is required for every agent leg entry.')
}
if (!agentLeg.last_name?.trim()) {
throw new PayloadValidationError('agentLegs.last_name is required for every agent leg entry.')
}
}
| customer_first_name: { | ||
| label: 'Customer First Name', | ||
| description: 'The first name for the customer.', | ||
| type: 'string', | ||
| default: { | ||
| '@path': '$.properties.customer_first_name' | ||
| } | ||
| }, |
| agent_first_name: { | ||
| label: 'Agent First Name', | ||
| description: 'The first name for the primary handling agent.', | ||
| type: 'string', | ||
| default: { | ||
| '@path': '$.properties.first_name' | ||
| '@path': '$.properties.agent_first_name' | ||
| } | ||
| }, | ||
| last_name: { | ||
| label: 'Last Name', | ||
| agent_last_name: { | ||
| label: 'Agent Last Name', | ||
| description: 'The last name for the primary handling agent.', | ||
| type: 'string', | ||
| default: { | ||
| '@path': '$.properties.last_name' | ||
| '@path': '$.properties.agent_last_name' | ||
| } | ||
| }, |
| function splitNameFromEmail(email?: string): { first_name?: string; last_name?: string } { | ||
| const localPart = email?.split('@')[0]?.split('+')[0] | ||
| const nameParts = localPart?.split(/[^a-zA-Z0-9]+/).filter(Boolean) ?? [] | ||
|
|
||
| if (nameParts.length === 0) { | ||
| return {} | ||
| } |
| function titleCase(value: string): string { | ||
| return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase() | ||
| } |
| agent_first_name: { | ||
| label: 'Agent First Name', | ||
| description: 'The first name for the primary handling agent.', | ||
| type: 'string', | ||
| default: { | ||
| '@path': '$.properties.first_name' | ||
| '@path': '$.properties.agent_first_name' | ||
| } | ||
| }, | ||
| last_name: { | ||
| label: 'Last Name', | ||
| agent_last_name: { | ||
| label: 'Agent Last Name', | ||
| description: 'The last name for the primary handling agent.', | ||
| type: 'string', | ||
| default: { | ||
| '@path': '$.properties.last_name' | ||
| '@path': '$.properties.agent_last_name' | ||
| } | ||
| }, |
| if (agentLeg.last_name === undefined || agentLeg.last_name === null) { | ||
| throw new PayloadValidationError( | ||
| 'agentLegs.last_name is required for every agent leg entry. Provide first_name and last_name, or enable assign_first_last_name_by_splitting_email and provide an agent_email that can be split. Single-token email local-parts derive an empty last_name.' | ||
| ) | ||
| } |
| /** | ||
| * When enabled, missing agent first and last names are derived from agent email addresses by splitting the email local-part. | ||
| */ | ||
| assign_first_last_name_by_splitting_email?: boolean |
|
Video of test: bf1496384aac109e26b89d1ab0529d13.webm |
|
Hi @toddtarsi thanks for the PR. Is this destination in use by customers yet? If any customers are already using the first_name and last_name fields, the data from these fields will not be collected any more. I would advise leaving the names of the fields as they were, and simply change the field labels and descriptions - doing this is not a breaking change. Please let me know how you'd like to proceed. |
|
@joe-ayoub-segment - That sounds good, will make that tweak instead. Thank you |
| agent_first_name: { | ||
| label: 'Agent First Name', | ||
| description: 'The first name for the primary handling agent.', | ||
| type: 'string', | ||
| default: { | ||
| '@path': '$.properties.first_name' | ||
| '@path': '$.properties.agent_first_name' | ||
| } | ||
| }, | ||
| last_name: { | ||
| label: 'Last Name', | ||
| agent_last_name: { | ||
| label: 'Agent Last Name', | ||
| description: 'The last name for the primary handling agent.', | ||
| type: 'string', | ||
| default: { | ||
| '@path': '$.properties.last_name' | ||
| '@path': '$.properties.agent_last_name' | ||
| } | ||
| }, |
| if (agentLeg.last_name === undefined || agentLeg.last_name === null) { | ||
| throw new PayloadValidationError( | ||
| 'agentLegs.last_name is required for every agent leg entry. Provide first_name and last_name, or enable assign_first_last_name_by_splitting_email and provide an agent_email that can be split. Single-token email local-parts derive an empty last_name.' | ||
| ) | ||
| } |
| agent_first_name: payload.agent_first_name || primaryAgentName.first_name, | ||
| agent_last_name: payload.agent_last_name || primaryAgentName.last_name, | ||
| agentLegs: payload.agentLegs?.map((agentLeg) => { | ||
| const splitName = splitNameFromEmail(agentLeg.agent_email) | ||
|
|
||
| return { | ||
| ...agentLeg, | ||
| first_name: agentLeg.first_name || splitName.first_name, | ||
| last_name: agentLeg.last_name || splitName.last_name | ||
| } | ||
| }) |
Thanks @toddtarsi . You also need to revert the name for the new agent_last_name field. |
d3551a2 to
4242bfc
Compare
4242bfc to
9afda04
Compare
| return { | ||
| ...payload, | ||
| first_name: shouldDeriveFirstName(payload.first_name) ? primaryAgentName.first_name : payload.first_name, | ||
| last_name: shouldDeriveLastName(payload.last_name) ? primaryAgentName.last_name : payload.last_name, | ||
| agentLegs: payload.agentLegs?.map((agentLeg) => { | ||
| const splitName = splitNameFromEmail(agentLeg.agent_email) | ||
|
|
||
| return { | ||
| ...agentLeg, | ||
| first_name: shouldDeriveFirstName(agentLeg.first_name) ? splitName.first_name : agentLeg.first_name, | ||
| last_name: shouldDeriveLastName(agentLeg.last_name) ? splitName.last_name : agentLeg.last_name | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
| function shouldDeriveLastName(value?: string): boolean { | ||
| return value === undefined || value === null || (value !== '' && value.trim() === '') | ||
| } |
| function titleCase(value: string): string { | ||
| return value.charAt(0).toUpperCase() + value.slice(1) | ||
| } |
|
@joe-ayoub-segment - Updated the fields to support the current customer and gives a path forward for better field naming going forward. Also, regenerated types |
|
@joe-ayoub-segment - Thank you for the review! |
| if ( | ||
| agentLeg.last_name === undefined || | ||
| agentLeg.last_name === null || | ||
| (agentLeg.last_name !== '' && agentLeg.last_name.trim() === '') | ||
| ) { | ||
| throw new PayloadValidationError( | ||
| 'agentLegs.last_name is required for every agent leg entry. Provide first_name and last_name, or enable assign_first_last_name_by_splitting_email and provide an agent_email that can be split. Single-token email local-parts derive an empty last_name.' | ||
| ) | ||
| } |
| function titleCase(value: string): string { | ||
| return value.charAt(0).toUpperCase() + value.slice(1) | ||
| } |
| if ( | ||
| agentLeg.last_name === undefined || | ||
| agentLeg.last_name === null || | ||
| (agentLeg.last_name !== '' && agentLeg.last_name.trim() === '') | ||
| ) { | ||
| throw new PayloadValidationError( | ||
| 'agentLegs.last_name is required for every agent leg entry. Provide first_name and last_name, or enable assign_first_last_name_by_splitting_email and provide an agent_email that can be split. Single-token email local-parts derive an empty last_name.' | ||
| ) | ||
| } |
|
Hi @toddtarsi , Apologies but I think you misunderstood me. there should be no need to mark some of the fields as legacy. instead, you could keep the fields named as they are, but give them different labels and descriptions. then when you are building the JSON to send to your platform you compose the JSON in the new shape you need it in. e.g. Keep the field named Also, I think you should remove the default, as this field is a simple boolean toggle, so the customer will probably not map payload data to it: You could set the default to true or false if you like: then the customer can change if if they want. |
|
Hi @toddtarsi just following up to see if this PR is still something you want to proceed with. If not I'll close it. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
packages/destination-actions/src/destinations/voiceops/sendCallCompleted/index.ts:39
- Derivation rules for last name are asymmetric with first name:
''is treated as “present” (so derivation won’t run), while whitespace-only triggers derivation. This means a mapped empty string (common when upstream fields are missing) will block email-based derivation even when enabled. Consider deriving whenvalueisundefined | nullorvalue.trim() === ''(including empty string), and rely on the post-derivation validation logic to allow''specifically when it comes from single-token email local-parts.
function shouldDeriveLastName(value?: string): boolean {
return value === undefined || value === null || (value !== '' && value.trim() === '')
}
packages/destination-actions/src/destinations/voiceops/sendCallCompleted/index.ts:21
emailis already guarded (if (!email || !EMAIL_REGEX.test(email)) return {}), so the optional chaining here is redundant and slightly obscures the invariants. Simplifying to non-optional calls (and avoiding?? []iflocalPartis always a string) would make the flow easier to follow.
const localPart = email?.split('@')[0]?.split('+')[0]
const nameParts = localPart?.split(/[^a-zA-Z0-9]+/).filter(Boolean) ?? []
| if ( | ||
| agentLeg.last_name === undefined || | ||
| agentLeg.last_name === null || | ||
| (agentLeg.last_name !== '' && agentLeg.last_name.trim() === '') | ||
| ) { | ||
| throw new PayloadValidationError( | ||
| 'agentLegs.last_name is required for every agent leg entry. Provide first_name and last_name, or enable assign_first_last_name_by_splitting_email and provide an agent_email that can be split. Single-token email local-parts derive an empty last_name.' | ||
| ) | ||
| } |
What changed
Adds explicit optional name mapping fields to the VoiceOps Send Call Completed action:
When email splitting is enabled, missing primary agent and agent-leg names are derived from the email local-part before the request is sent to VoiceOps. For example, ava-agent@voiceops.com derives Ava / Agent.
Why
VoiceOps customers need clearer mapping controls for customer and agent names, plus a fallback for deriving agent names from email addresses when names are not provided by the source event.
Compatibility
Validation
Note: the local pre-commit hook could not run because it invokes yarn directly and yarn is not on PATH in this shell; the equivalent build/test checks above were run manually.
Segment review note
Before marking this ready for review, attach an Action Tester video showing the new mapping fields and email-splitting behavior with a realistic Call Completed payload.