feat: migrate to new transcript API#89
Conversation
WalkthroughThe PR refactors Voiceflow transcript handling by removing outdated type definitions, introducing streamlined response types for v1 APIs, and restructuring three main transcript-fetching functions to use paginated POST requests, new v1 endpoints, and log-based responses instead of previous patterns. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Caller
participant TJ as FetchTranscriptJSON
participant API as Voiceflow v1 API
participant LTT as logsToTurns
Client->>TJ: FetchTranscriptJSON(agentID, transcriptID)
TJ->>API: GET /v1/transcript/{transcriptID}
API-->>TJ: GetTranscriptResponse{Logs: [...]}
TJ->>LTT: logsToTurns(logs)
LTT->>LTT: Parse log.CreatedAt (RFC3339)
LTT->>LTT: Map log.Type & logData.Type to Turn
LTT-->>TJ: []transcript.Turn
TJ-->>Client: []transcript.Turn (or error)
sequenceDiagram
participant Client as Caller
participant FTI as FetchTranscriptInformations
participant API as Voiceflow v1 Analytics
Client->>FTI: FetchTranscriptInformations(agentID, startTime, endTime, ...)
loop Paginated (skip/take)
FTI->>FTI: Build SearchTranscriptsRequest
FTI->>API: POST /v1/analytics/transcripts
API-->>FTI: SearchTranscriptsResponse{Transcripts: [...]}
FTI->>FTI: Accumulate results
alt Page has fewer results than pageSize
FTI->>FTI: Break loop (last page)
end
end
FTI-->>Client: []transcript.TranscriptInformation (or error)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/transcript/to-test.go`:
- Around line 82-100: In the "user-text" case the code does a silent type
assertion userText, _ := turn.Payload.Payload.(string) which turns
invalid/malformed payloads into empty user steps; update the handler to perform
a checked assertion (userText, ok := turn.Payload.Payload.(string)) and if ok is
false return or propagate an error (or log and skip) so invalid payloads are
rejected instead of producing bogus tests; ensure the flow around
findNextAgentTextResponse and the construction of tests.Interaction still uses
the validated userText when ok is true and generate a clear error path when not.
In `@pkg/voiceflow/transcript.go`:
- Around line 86-104: The CSV export loop in transcript.go currently sends all
payload_type == "text" through GetTextPayload(), which causes raw user
utterances stored as "user-text" to be exported blank; update the switch in the
loop over turns to handle "user-text" as its own case (before/alongside "text")
and set message from the raw string stored on the payload (i.e., use the
payload's raw utterance field on turn.Payload rather than calling
GetTextPayload()); locate the switch on turn.Payload.Type and add a case
"user-text" that assigns the raw utterance to message so those rows export
correctly.
- Around line 150-207: The loop currently swallows JSON/time decode errors
(json.Unmarshal(log.Data, &data), time.Parse(log.CreatedAt), and nested
json.Unmarshal on data.Payload) which corrupts transcript turns; change these to
propagate errors instead of continuing: on failure of json.Unmarshal(log.Data,
...), return the error (or wrap with context indicating the offending log), on
time.Parse return a parse error (do not ignore zero time), and on any nested
payload unmarshal (for intent/text/trace) return the unmarshal error; update the
enclosing function signature (the function that builds turns from logs in
pkg/voiceflow/transcript.go) and its callers to accept/handle the returned error
so malformed logs fail fast rather than producing empty/incorrect turns.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3bfc5740-6412-4ccb-9afb-f9d2dbbb923c
📒 Files selected for processing (5)
internal/types/voiceflow/transcript/fetchTranscriptInformationsResponseTypes copy.gointernal/types/voiceflow/transcript/fetchTranscriptInformationsResponseTypes.gointernal/types/voiceflow/transcript/fetchTranscriptJSONResponseTypes.gopkg/transcript/to-test.gopkg/voiceflow/transcript.go
💤 Files with no reviewable changes (1)
- internal/types/voiceflow/transcript/fetchTranscriptInformationsResponseTypes copy.go
| case "user-text": | ||
| userText, _ := turn.Payload.Payload.(string) | ||
| agentResponse := findNextAgentTextResponse(transcriptJSON, index) | ||
| test.Interactions = append(test.Interactions, tests.Interaction{ | ||
| ID: uuid.New().String(), | ||
| User: tests.User{ | ||
| Text: userText, | ||
| Type: "text", | ||
| }, | ||
| Agent: tests.Agent{ | ||
| Validate: []tests.Validation{ | ||
| { | ||
| ID: uuid.New().String(), | ||
| Type: "equals", | ||
| Value: agentResponse, | ||
| }, | ||
| }, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Reject invalid user-text payloads instead of silently emitting empty steps.
userText, _ := turn.Payload.Payload.(string) falls back to "" on any shape mismatch, so a bad decode upstream turns into a bogus test case rather than an error.
Proposed fix
case "user-text":
- userText, _ := turn.Payload.Payload.(string)
+ userText, ok := turn.Payload.Payload.(string)
+ if !ok {
+ return tests.Test{}, fmt.Errorf("unexpected user-text payload type %T", turn.Payload.Payload)
+ }
agentResponse := findNextAgentTextResponse(transcriptJSON, index)
test.Interactions = append(test.Interactions, tests.Interaction{📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case "user-text": | |
| userText, _ := turn.Payload.Payload.(string) | |
| agentResponse := findNextAgentTextResponse(transcriptJSON, index) | |
| test.Interactions = append(test.Interactions, tests.Interaction{ | |
| ID: uuid.New().String(), | |
| User: tests.User{ | |
| Text: userText, | |
| Type: "text", | |
| }, | |
| Agent: tests.Agent{ | |
| Validate: []tests.Validation{ | |
| { | |
| ID: uuid.New().String(), | |
| Type: "equals", | |
| Value: agentResponse, | |
| }, | |
| }, | |
| }, | |
| }) | |
| case "user-text": | |
| userText, ok := turn.Payload.Payload.(string) | |
| if !ok { | |
| return tests.Test{}, fmt.Errorf("unexpected user-text payload type %T", turn.Payload.Payload) | |
| } | |
| agentResponse := findNextAgentTextResponse(transcriptJSON, index) | |
| test.Interactions = append(test.Interactions, tests.Interaction{ | |
| ID: uuid.New().String(), | |
| User: tests.User{ | |
| Text: userText, | |
| Type: "text", | |
| }, | |
| Agent: tests.Agent{ | |
| Validate: []tests.Validation{ | |
| { | |
| ID: uuid.New().String(), | |
| Type: "equals", | |
| Value: agentResponse, | |
| }, | |
| }, | |
| }, | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/transcript/to-test.go` around lines 82 - 100, In the "user-text" case the
code does a silent type assertion userText, _ := turn.Payload.Payload.(string)
which turns invalid/malformed payloads into empty user steps; update the handler
to perform a checked assertion (userText, ok := turn.Payload.Payload.(string))
and if ok is false return or propagate an error (or log and skip) so invalid
payloads are rejected instead of producing bogus tests; ensure the flow around
findNextAgentTextResponse and the construction of tests.Interaction still uses
the validated userText when ok is true and generate a clear error path when not.
| for _, turn := range turns { | ||
| message := "" | ||
| switch turn.Payload.Type { | ||
| case "text": | ||
| if tp, err := turn.Payload.GetTextPayload(); err == nil { | ||
| message = tp.Message | ||
| } | ||
| case "intent": | ||
| if ip, err := turn.Payload.GetIntentPayload(); err == nil { | ||
| message = ip.Query | ||
| } | ||
| } | ||
|
|
||
| rows = append(rows, []string{ | ||
| turn.Type, | ||
| turn.Payload.Type, | ||
| message, | ||
| turn.StartTime.Format(time.RFC3339), | ||
| }) |
There was a problem hiding this comment.
Handle user-text rows separately in CSV export.
The new log conversion stores user utterances as raw strings, but this branch routes all payload_type == "text" turns through GetTextPayload(). As a result, user-text rows lose their message content and export as blank.
Proposed fix
for _, turn := range turns {
message := ""
- switch turn.Payload.Type {
- case "text":
- if tp, err := turn.Payload.GetTextPayload(); err == nil {
- message = tp.Message
- }
- case "intent":
- if ip, err := turn.Payload.GetIntentPayload(); err == nil {
- message = ip.Query
- }
+ switch turn.Type {
+ case "user-text":
+ userText, ok := turn.Payload.Payload.(string)
+ if !ok {
+ return nil, fmt.Errorf("unexpected user-text payload type %T", turn.Payload.Payload)
+ }
+ message = userText
+ default:
+ switch turn.Payload.Type {
+ case "text":
+ if tp, err := turn.Payload.GetTextPayload(); err == nil {
+ message = tp.Message
+ }
+ case "intent":
+ if ip, err := turn.Payload.GetIntentPayload(); err == nil {
+ message = ip.Query
+ }
+ }
}
rows = append(rows, []string{📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for _, turn := range turns { | |
| message := "" | |
| switch turn.Payload.Type { | |
| case "text": | |
| if tp, err := turn.Payload.GetTextPayload(); err == nil { | |
| message = tp.Message | |
| } | |
| case "intent": | |
| if ip, err := turn.Payload.GetIntentPayload(); err == nil { | |
| message = ip.Query | |
| } | |
| } | |
| rows = append(rows, []string{ | |
| turn.Type, | |
| turn.Payload.Type, | |
| message, | |
| turn.StartTime.Format(time.RFC3339), | |
| }) | |
| for _, turn := range turns { | |
| message := "" | |
| switch turn.Type { | |
| case "user-text": | |
| userText, ok := turn.Payload.Payload.(string) | |
| if !ok { | |
| return nil, fmt.Errorf("unexpected user-text payload type %T", turn.Payload.Payload) | |
| } | |
| message = userText | |
| default: | |
| switch turn.Payload.Type { | |
| case "text": | |
| if tp, err := turn.Payload.GetTextPayload(); err == nil { | |
| message = tp.Message | |
| } | |
| case "intent": | |
| if ip, err := turn.Payload.GetIntentPayload(); err == nil { | |
| message = ip.Query | |
| } | |
| } | |
| } | |
| rows = append(rows, []string{ | |
| turn.Type, | |
| turn.Payload.Type, | |
| message, | |
| turn.StartTime.Format(time.RFC3339), | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/voiceflow/transcript.go` around lines 86 - 104, The CSV export loop in
transcript.go currently sends all payload_type == "text" through
GetTextPayload(), which causes raw user utterances stored as "user-text" to be
exported blank; update the switch in the loop over turns to handle "user-text"
as its own case (before/alongside "text") and set message from the raw string
stored on the payload (i.e., use the payload's raw utterance field on
turn.Payload rather than calling GetTextPayload()); locate the switch on
turn.Payload.Type and add a case "user-text" that assigns the raw utterance to
message so those rows export correctly.
| for _, log := range logs { | ||
| var data transcript.LogData | ||
| if err := json.Unmarshal(log.Data, &data); err != nil { | ||
| continue | ||
| } | ||
|
|
||
| createdAt, _ := time.Parse(time.RFC3339, log.CreatedAt) | ||
|
|
||
| switch log.Type { | ||
| case "action": | ||
| turn := transcript.Turn{ | ||
| StartTime: createdAt, | ||
| } | ||
| switch data.Type { | ||
| case "launch": | ||
| turn.Type = "launch" | ||
| case "intent": | ||
| turn.Type = "request" | ||
| turn.Payload = transcript.Payload{ | ||
| Type: "intent", | ||
| } | ||
| if data.Payload != nil { | ||
| var payloadData interface{} | ||
| _ = json.Unmarshal(data.Payload, &payloadData) | ||
| turn.Payload.Payload = payloadData | ||
| } | ||
| case "text": | ||
| // User text input — payload is a plain string | ||
| turn.Type = "user-text" | ||
| var userText string | ||
| if data.Payload != nil { | ||
| _ = json.Unmarshal(data.Payload, &userText) | ||
| } | ||
| turn.Payload = transcript.Payload{ | ||
| Type: "text", | ||
| Payload: userText, | ||
| } | ||
| default: | ||
| continue | ||
| } | ||
| turns = append(turns, turn) | ||
|
|
||
| case "trace": | ||
| if data.Type != "text" { | ||
| continue | ||
| } | ||
| var payloadData interface{} | ||
| if data.Payload != nil { | ||
| _ = json.Unmarshal(data.Payload, &payloadData) | ||
| } | ||
| turns = append(turns, transcript.Turn{ | ||
| Type: "text", | ||
| Payload: transcript.Payload{ | ||
| Type: "text", | ||
| Payload: payloadData, | ||
| }, | ||
| StartTime: createdAt, | ||
| }) |
There was a problem hiding this comment.
Don’t silently coerce malformed transcript logs into valid-looking turns.
This path drops decode errors from log.Data, log.CreatedAt, and nested payloads. A partially incompatible API response will produce zero timestamps or empty payloads, and downstream CSV/test generation will quietly emit corrupted data instead of failing fast.
Proposed fix
for _, log := range logs {
var data transcript.LogData
if err := json.Unmarshal(log.Data, &data); err != nil {
- continue
+ return nil, fmt.Errorf("failed to unmarshal log data: %w", err)
}
- createdAt, _ := time.Parse(time.RFC3339, log.CreatedAt)
+ createdAt, err := time.Parse(time.RFC3339, log.CreatedAt)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse log timestamp %q: %w", log.CreatedAt, err)
+ }
@@
if data.Payload != nil {
var payloadData interface{}
- _ = json.Unmarshal(data.Payload, &payloadData)
+ if err := json.Unmarshal(data.Payload, &payloadData); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal intent payload: %w", err)
+ }
turn.Payload.Payload = payloadData
}
@@
var userText string
if data.Payload != nil {
- _ = json.Unmarshal(data.Payload, &userText)
+ if err := json.Unmarshal(data.Payload, &userText); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal user text payload: %w", err)
+ }
}
@@
var payloadData interface{}
if data.Payload != nil {
- _ = json.Unmarshal(data.Payload, &payloadData)
+ if err := json.Unmarshal(data.Payload, &payloadData); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal trace text payload: %w", err)
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for _, log := range logs { | |
| var data transcript.LogData | |
| if err := json.Unmarshal(log.Data, &data); err != nil { | |
| continue | |
| } | |
| createdAt, _ := time.Parse(time.RFC3339, log.CreatedAt) | |
| switch log.Type { | |
| case "action": | |
| turn := transcript.Turn{ | |
| StartTime: createdAt, | |
| } | |
| switch data.Type { | |
| case "launch": | |
| turn.Type = "launch" | |
| case "intent": | |
| turn.Type = "request" | |
| turn.Payload = transcript.Payload{ | |
| Type: "intent", | |
| } | |
| if data.Payload != nil { | |
| var payloadData interface{} | |
| _ = json.Unmarshal(data.Payload, &payloadData) | |
| turn.Payload.Payload = payloadData | |
| } | |
| case "text": | |
| // User text input — payload is a plain string | |
| turn.Type = "user-text" | |
| var userText string | |
| if data.Payload != nil { | |
| _ = json.Unmarshal(data.Payload, &userText) | |
| } | |
| turn.Payload = transcript.Payload{ | |
| Type: "text", | |
| Payload: userText, | |
| } | |
| default: | |
| continue | |
| } | |
| turns = append(turns, turn) | |
| case "trace": | |
| if data.Type != "text" { | |
| continue | |
| } | |
| var payloadData interface{} | |
| if data.Payload != nil { | |
| _ = json.Unmarshal(data.Payload, &payloadData) | |
| } | |
| turns = append(turns, transcript.Turn{ | |
| Type: "text", | |
| Payload: transcript.Payload{ | |
| Type: "text", | |
| Payload: payloadData, | |
| }, | |
| StartTime: createdAt, | |
| }) | |
| for _, log := range logs { | |
| var data transcript.LogData | |
| if err := json.Unmarshal(log.Data, &data); err != nil { | |
| return nil, fmt.Errorf("failed to unmarshal log data: %w", err) | |
| } | |
| createdAt, err := time.Parse(time.RFC3339, log.CreatedAt) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to parse log timestamp %q: %w", log.CreatedAt, err) | |
| } | |
| switch log.Type { | |
| case "action": | |
| turn := transcript.Turn{ | |
| StartTime: createdAt, | |
| } | |
| switch data.Type { | |
| case "launch": | |
| turn.Type = "launch" | |
| case "intent": | |
| turn.Type = "request" | |
| turn.Payload = transcript.Payload{ | |
| Type: "intent", | |
| } | |
| if data.Payload != nil { | |
| var payloadData interface{} | |
| if err := json.Unmarshal(data.Payload, &payloadData); err != nil { | |
| return nil, fmt.Errorf("failed to unmarshal intent payload: %w", err) | |
| } | |
| turn.Payload.Payload = payloadData | |
| } | |
| case "text": | |
| // User text input — payload is a plain string | |
| turn.Type = "user-text" | |
| var userText string | |
| if data.Payload != nil { | |
| if err := json.Unmarshal(data.Payload, &userText); err != nil { | |
| return nil, fmt.Errorf("failed to unmarshal user text payload: %w", err) | |
| } | |
| } | |
| turn.Payload = transcript.Payload{ | |
| Type: "text", | |
| Payload: userText, | |
| } | |
| default: | |
| continue | |
| } | |
| turns = append(turns, turn) | |
| case "trace": | |
| if data.Type != "text" { | |
| continue | |
| } | |
| var payloadData interface{} | |
| if data.Payload != nil { | |
| if err := json.Unmarshal(data.Payload, &payloadData); err != nil { | |
| return nil, fmt.Errorf("failed to unmarshal trace text payload: %w", err) | |
| } | |
| } | |
| turns = append(turns, transcript.Turn{ | |
| Type: "text", | |
| Payload: transcript.Payload{ | |
| Type: "text", | |
| Payload: payloadData, | |
| }, | |
| StartTime: createdAt, | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/voiceflow/transcript.go` around lines 150 - 207, The loop currently
swallows JSON/time decode errors (json.Unmarshal(log.Data, &data),
time.Parse(log.CreatedAt), and nested json.Unmarshal on data.Payload) which
corrupts transcript turns; change these to propagate errors instead of
continuing: on failure of json.Unmarshal(log.Data, ...), return the error (or
wrap with context indicating the offending log), on time.Parse return a parse
error (do not ignore zero time), and on any nested payload unmarshal (for
intent/text/trace) return the unmarshal error; update the enclosing function
signature (the function that builds turns from logs in
pkg/voiceflow/transcript.go) and its callers to accept/handle the returned error
so malformed logs fail fast rather than producing empty/incorrect turns.
Migrate to new Transcript API
Summary by CodeRabbit