Repository navigation
VAPI-4042: emit Twilio's Gather/Record defaults and clamp to BW's ranges - #32
Open
mramasubramanian-bw wants to merge 3 commits into
Open
mramasubramanian-bw wants to merge 3 commits into
mramasubramanian-bw wants to merge 3 commits into
Conversation
Twilio and Bandwidth default differently for several Gather/Record attributes; leaving them unset let Bandwidth's own default silently change IVR behavior instead of matching Twilio's. Gather: - finishOnKey omitted -> emit "#" (Twilio's default); Bandwidth's own default is "" (disabled). finishOnKey="" still passes through. - timeout omitted -> default "5", set as both firstDigitTimeout and interDigitTimeout (Bandwidth splits what Twilio combines). - numDigits/timeout clamped to Bandwidth's documented ranges with a warning naming the original value. - A Gather with no action re-requests the current document, matching Twilio, when the server provides the document's URL. Record: - maxLength omitted -> emit "3600" (Twilio's default); Bandwidth's own default is 60s. - timeout (silence ends the recording) is now mapped at all, to silenceTimeout; previously dropped entirely. timeout="0" (Twilio: disable) and silenceTimeout="0" (Bandwidth: no timeout) agree. - finishOnKey narrowed to Bandwidth's single terminating digit (preferring #, then *, then the first character), with a warning, since Bandwidth can't represent Twilio's multi-key set (default: any key). A single explicit digit passes through unchanged. - playBeep/trim now warn on the omitted (Twilio-default) case too, not only when explicitly set to a non-matching value. - A Record with no action re-requests the current document, matching Twilio, when the server provides the document's URL. The "re-request current document" behavior is driven by a new TranslateOptions.documentUrl, set only by the server (one line in app.ts); standalone BXML generation leaves it unset and keeps the original warning, since there's no live document for it to re-fetch.
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
The warning a customer sees when a value is outside Bandwidth's range
said "clamped to X" instead of plainly saying what happened. It now
says "using X instead". The matrix notes, AGENTS.md, and the new test
names follow the same change ("kept within Bandwidth's limits" /
"limits X to Bandwidth's maximum" rather than "clamped").
The clampAttr function name and its internal comments are unchanged;
"clamp" was already an internal term in this file (the Say/Play loop
count logic), so this only touches text a person actually reads.
- "" on timeout/maxLength was read as 0 (Number("") === 0), not as
missing: <Record maxLength=""/> cut voicemail to 1 second, and
<Gather timeout=""/> sent an empty firstDigitTimeout. Treated as
missing now (clampAttr's callers use || instead of ?? for these).
- A value that isn't a plain finite number ("abc", "Infinity") now
warns and falls back to Twilio's default, instead of being copied
into the BXML as-is for Bandwidth to reject. A value that IS a
number but an unusual form ("0x3C", "1e1") is normalized to a plain
decimal instead of being emitted as written.
- Record's finishOnKey narrowing now only picks a real keypad
character: "ab1" narrows to "1", not "a", and a value with no digit,
"*", or "#" at all (e.g. "a") is dropped instead of passed through,
so Bandwidth's own default applies.
- Range warnings now name the Twilio attribute the customer actually
wrote (timeout, maxLength) rather than only Bandwidth's name for it
(firstDigitTimeout, maxDuration), while still saying which BXML
attribute is affected.
- Identical findings (verb + message) collapse to one per document, so
a document with many ordinary bare Records doesn't bury a real
out-of-range warning under repeats of the same generic one.
- Corrected AGENTS.md: Bandwidth already defaults firstDigitTimeout and
interDigitTimeout to 5s on its own, so an omitted Gather timeout
isn't a behavior change; the real fix is that an explicit timeout
previously only reached firstDigitTimeout. Also listed maxLength
among the range-limited attributes and stopped calling finishOnKey's
narrowing a range limit.
- Pulled the duplicated "no action -> re-request the current document,
else warn" branching out of translateGather/translateRecord into one
resolveActionUrl helper.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
<Gather>/<Record>now use Twilio's own default values instead of letting Bandwidth's different defaults silently change IVR behavior:finishOnKey,timeout→firstDigitTimeout/interDigitTimeout, Record'smaxLength, and Record'stimeout→silenceTimeout(this wasn't translated to anything before).numDigits/timeout/maxLengthare now kept within the limits Bandwidth documents, with a warning, instead of letting an out-of-range value through for Bandwidth to reject. A value that isn't a plain number at all (empty,"abc", a hex or scientific-notation string) now warns and falls back to Twilio's own default, instead of being copied into the BXML as written.finishOnKeyis cut down to the one real keypad key (a digit,*, or#) Bandwidth'sRecordcan use to stop — preferring#, then*, then the first valid one present — since Bandwidth can't use a whole set of keys the way Twilio does. A value with no valid key at all (e.g. a letter) is dropped rather than passed through.<Gather>/<Record>with noactionnow sends its result back to the same page that served it, matching Twilio, when the server knows that page's address.playBeep/trimnow warn when they're left out too (Twilio's default plays a beep and trims silence), not only when someone sets them to a different value on purpose.Motivation / Context
translateGather/translateRecordonly copied an attribute when the TwiML set it. Where Twilio's default is different from Bandwidth's, the translated verb behaved differently with no warning: a default<Record>cut voicemail off at 60 seconds with no way to stop on silence, a default<Gather>counted#as just another digit and could cut an entry short mid-way, and a value outside Bandwidth's limits made Bandwidth reject the whole call. Tracked in VAPI-4042.Reviewer Notes
TranslateOptions.documentUrl, set only by the server (one line inapp.ts, next to the existingrewriteUrl). The standalone BXML generator (src/bxml-generator) leaves it unset and keeps the original warning, since it has no live server to send a request back to — doing this there would just produce a useless emptygatherUrl=""in a static file.timeoutsets bothfirstDigitTimeoutandinterDigitTimeout, each kept within its own limits (0-60 and 1-60). One Twilio value can land differently for each — e.g.timeout="0"leavesfirstDigitTimeout="0"alone but raisesinterDigitTimeoutto1— so these are two separate warnings, not one.finishOnKey: Bandwidth's docs say "this digit" (singular), so a Twilio value with more than one character (including the implicit default, any of1234567890*#) can only be reduced to one character — and only to a character Bandwidth actually recognizes as a key (a digit,*, or#). A single such character someone set on purpose passes through with no warning, since that's an exact match.playBeep/trimwarn whenever Twilio's own behavior (play a beep / trim silence) wouldn't happen on Bandwidth — including when they're left out, since Twilio's defaults for both are "yes, do it" (playBeepdefaults totrue,trimdefaults totrim-silence). The one setting that doesn't warn for each (playBeep="false",trim="do-not-trim") is the one case where Bandwidth's fixed behavior (never beeps, never trims) actually matches.Record timeout="0"(Twilio: turn off silence detection) and Bandwidth's ownsilenceTimeout="0"(no timeout) already mean the same thing, so that value needed no special handling.timeout/maxLengthset to an empty string (timeout="") is treated the same as leaving the attribute out, not as the number zero — an empty string isn't a value a customer would set on purpose, so there's no warning either, just the same default used for the missing case. An emptyfinishOnKeyis handled differently, since both Twilio and Bandwidth treat that specific value as "turn the stop-key feature off" on purpose.timeout,maxLength), not only Bandwidth's name for it (firstDigitTimeout,maxDuration), while still saying which Bandwidth attribute is affected — so the warning can be found by searching the customer's own TwiML.<Record>s previously repeated the same 3 generic warnings for every one of them, which could bury a real out-of-range warning under dozens of repeats of an unrelated one.translateGatherandtranslateRecordis now one shared helper.Breaking Changes
<Gather>(nofinishOnKey) now stops on#, matching Twilio; before,#was just another digit (Bandwidth's own default turns off the stop-key feature). Any flow that expected#to be collected along with the other digits will now stop there instead.<Record>(notimeout) now stops after 5 seconds of silence (silenceTimeout), which had no equivalent at all before — a long pause mid-recording can now end it sooner than it used to.<Gather>/<Record>with noactionattribute now sends a real request back to the customer's current page (carryingDigits/RecordingUrl, etc.), instead of Bandwidth quietly dropping the result (Gather) or moving on to the next verb once the recording ends (Record). This matches Twilio, but it's a request shape that page may not have expected before.numDigits/timeout/maxLengthoutside Bandwidth's documented range, or not a plain number at all, are now brought back into range (with a warning) instead of passed through as written, which used to risk Bandwidth rejecting the call outright.<Gather>with notimeoutset does not change behavior: Bandwidth already defaultsfirstDigitTimeout/interDigitTimeoutto 5 seconds on its own, matching Twilio. What does change: an explicittimeoutpreviously only reachedfirstDigitTimeout, silently leavinginterDigitTimeoutat Bandwidth's 5-second default regardless of what was set — now it governs both, matching Twilio's singletimeoutvalue doing both jobs. (An earlier version of this PR description got this backwards; a reviewer caught it.)Tests
Added
Gather > Twilio defaults› emits finishOnKey's Twilio default (#) when omittedterminatingDigits="#"whenfinishOnKeyisn't settimeoutstill sets both attributesGather > keeping values within Bandwidth's limits› brings an out-of-range timeout back within limits for both firstDigitTimeout and interDigitTimeout, with a warning for eachtimeout="0"only raises one of the two attributesnumDigitsis brought into range with a warningNumber("")is0; an empty value must not be read as a real zerotimeout="0x3C"becomes"60", not left as the hex stringnumDigits="1e1"becomes"10"numDigits="abc"becomes"50"with a warning, not passed through as"abc"Gather > no action, re-requesting the current document› points gatherUrl at the current document when the server provides one, with no warningdocumentUrlset →gatherUrlpoints back at the same page; the Gather's position and empty-result flag still travel throughRecord > Twilio defaults› emits maxLength's Twilio default (3600) when omittedmaxDuration="3600", not Bandwidth's 60-second defaultsilenceTimeoutis translated to something at all nowRecord > keeping values within Bandwidth's limits› limits maxLength to Bandwidth's 10800s (3h) maximumsilenceTimeoutmaxLengthmust not cut recordings to 1 secondtimeoutRecord > finishOnKey -> a single terminatingDigit› warns when omitted#default only covers one of Twilio's "any key" set#is picked over the other characters in the set#isn't an optionfinishOnKey="ab1"picks"1", not"a"finishOnKey="a"leavesterminatingDigitsunset instead of emitting"a"Record > playBeep and trim› warns when playBeep is omitted and when explicitly trueRecord > no action, re-requesting the current document› points recordCompleteUrl at the current document when the server provides onerecordCompleteUrlpoints back at the same page, no "missing action" warningrepeated identical findings collapse to one› a document with many bare Records reports each generic warning once, not once per Record<Record>s produce oneplayBeepwarning, not ten<Record>s with different out-of-rangemaxLengths each keep their own warningGather/Record with no action re-request the current document, like Twilio› Gather posts Digits back to the same voice URL that served itgenerate → band BXML conformance› band accepts generated BXML for gather-record-limits.xmlfinishOnKey=""/multi-character cases, checked against Bandwidth's own CLIModified
Gather› maps attributes and nested promptsinterDigitTimeout="7"timeoutnow sets both attributes, not justfirstDigitTimeoutTest Plan
npm run typechecknpm test, includingtest/bxml-generator-band-conformance.test.tswith thebandCLI installedfinishOnKey, and the no-action case sending results back to the original voice page;band bxml rawaccepted every documentfinishOnKeyhandling; the attribute-name mix-up in warnings; the repeated-findings issue)finishOnKeystops on#, and the#is not included inDigitstimeoutstops on silence (silenceTimeout), not at themaxLengthsafety capactionsends its result back to the page that served it, with the caller's digits/recording