Skip to content

feat: migrate to new transcript API#89

Merged
xavidop merged 1 commit into
mainfrom
xavier/update-transcripts/IN-3829
Apr 10, 2026
Merged

feat: migrate to new transcript API#89
xavidop merged 1 commit into
mainfrom
xavier/update-transcripts/IN-3829

Conversation

@xavidop

@xavidop xavidop commented Apr 10, 2026

Copy link
Copy Markdown
Owner

Migrate to new Transcript API

Summary by CodeRabbit

  • Refactoring
    • Updated transcript API response handling for improved data processing and pagination support.
    • Modernized transcript search and retrieval mechanisms with enhanced filtering and pagination.
    • Improved CSV export generation to better match current transcript data structures.

@mergify mergify Bot added the size/L label Apr 10, 2026
@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown

Walkthrough

The 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

Cohort / File(s) Summary
Type Definitions Restructuring
internal/types/voiceflow/transcript/fetchTranscriptInformationsResponseTypes copy.go, internal/types/voiceflow/transcript/fetchTranscriptInformationsResponseTypes.go, internal/types/voiceflow/transcript/fetchTranscriptJSONResponseTypes.go
Removed outdated Annotation and legacy TranscriptInformation types. Introduced new streamlined types: TranscriptInformation, SearchTranscriptsRequest, SearchTranscriptsResponse for v1 API integration. Refactored response wrapper types to use GetTranscriptResponse, TranscriptDetail, Log, and LogData; simplified TextPayload and IntentPayload structures; removed BlockPayload, FlowPayload, DebugPayload, and RequestPayload types.
Transcript API Integration
pkg/voiceflow/transcript.go
Replaced FetchTranscriptInformations with paginated POST workflow using skip/take parameters. Refactored FetchTranscriptJSON from v2 direct unmarshal to v1 endpoint with log-to-turn conversion via new logsToTurns helper. Rewrote FetchTranscriptCSV to derive rows from FetchTranscriptJSON output instead of separate endpoint. Added explicit HTTP status and marshaling error handling across all three functions.
Test Helper Updates
pkg/transcript/to-test.go
Added handling for turn.Type == "user-text" case in TranscriptToTest, creating interactions with user text and agent response validation.

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)
Loading
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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hopping through transcripts so fine,
Old types cleared up, new v1 shines,
Logs bloom as Turns, pagination flows,
Refactored by careful reviewers' prose! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: migrate to new transcript API' accurately captures the main objective of this pull request, which involves updating transcript-related functionality to use a new API version.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch xavier/update-transcripts/IN-3829

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 70fe5f5 and 1961eda.

📒 Files selected for processing (5)
  • internal/types/voiceflow/transcript/fetchTranscriptInformationsResponseTypes copy.go
  • internal/types/voiceflow/transcript/fetchTranscriptInformationsResponseTypes.go
  • internal/types/voiceflow/transcript/fetchTranscriptJSONResponseTypes.go
  • pkg/transcript/to-test.go
  • pkg/voiceflow/transcript.go
💤 Files with no reviewable changes (1)
  • internal/types/voiceflow/transcript/fetchTranscriptInformationsResponseTypes copy.go

Comment thread pkg/transcript/to-test.go
Comment on lines +82 to +100
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,
},
},
},
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +86 to +104
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),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +150 to +207
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,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

@xavidop xavidop merged commit 81e6041 into main Apr 10, 2026
11 checks passed
@xavidop xavidop deleted the xavier/update-transcripts/IN-3829 branch April 10, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants