/ components / FeedbackSurvey / submitTranscriptShare.ts
submitTranscriptShare.ts
  1  import axios from 'axios'
  2  import { readFile, stat } from 'fs/promises'
  3  import type { Message } from '../../types/message.js'
  4  import { checkAndRefreshOAuthTokenIfNeeded } from '../../utils/auth.js'
  5  import { logForDebugging } from '../../utils/debug.js'
  6  import { errorMessage } from '../../utils/errors.js'
  7  import { getAuthHeaders, getUserAgent } from '../../utils/http.js'
  8  import { normalizeMessagesForAPI } from '../../utils/messages.js'
  9  import {
 10    extractAgentIdsFromMessages,
 11    getTranscriptPath,
 12    loadSubagentTranscripts,
 13    MAX_TRANSCRIPT_READ_BYTES,
 14  } from '../../utils/sessionStorage.js'
 15  import { jsonStringify } from '../../utils/slowOperations.js'
 16  import { redactSensitiveInfo } from '../Feedback.js'
 17  
 18  type TranscriptShareResult = {
 19    success: boolean
 20    transcriptId?: string
 21  }
 22  
 23  export type TranscriptShareTrigger =
 24    | 'bad_feedback_survey'
 25    | 'good_feedback_survey'
 26    | 'frustration'
 27    | 'memory_survey'
 28  
 29  export async function submitTranscriptShare(
 30    messages: Message[],
 31    trigger: TranscriptShareTrigger,
 32    appearanceId: string,
 33  ): Promise<TranscriptShareResult> {
 34    try {
 35      logForDebugging('Collecting transcript for sharing', { level: 'info' })
 36  
 37      const transcript = normalizeMessagesForAPI(messages)
 38  
 39      // Collect subagent transcripts
 40      const agentIds = extractAgentIdsFromMessages(messages)
 41      const subagentTranscripts = await loadSubagentTranscripts(agentIds)
 42  
 43      // Read raw JSONL transcript (with size guard to prevent OOM)
 44      let rawTranscriptJsonl: string | undefined
 45      try {
 46        const transcriptPath = getTranscriptPath()
 47        const { size } = await stat(transcriptPath)
 48        if (size <= MAX_TRANSCRIPT_READ_BYTES) {
 49          rawTranscriptJsonl = await readFile(transcriptPath, 'utf-8')
 50        } else {
 51          logForDebugging(
 52            `Skipping raw transcript read: file too large (${size} bytes)`,
 53            { level: 'warn' },
 54          )
 55        }
 56      } catch {
 57        // File may not exist
 58      }
 59  
 60      const data = {
 61        trigger,
 62        version: MACRO.VERSION,
 63        platform: process.platform,
 64        transcript,
 65        subagentTranscripts:
 66          Object.keys(subagentTranscripts).length > 0
 67            ? subagentTranscripts
 68            : undefined,
 69        rawTranscriptJsonl,
 70      }
 71  
 72      const content = redactSensitiveInfo(jsonStringify(data))
 73  
 74      await checkAndRefreshOAuthTokenIfNeeded()
 75  
 76      const authResult = getAuthHeaders()
 77      if (authResult.error) {
 78        return { success: false }
 79      }
 80  
 81      const headers: Record<string, string> = {
 82        'Content-Type': 'application/json',
 83        'User-Agent': getUserAgent(),
 84        ...authResult.headers,
 85      }
 86  
 87      const response = await axios.post(
 88        'https://api.anthropic.com/api/claude_code_shared_session_transcripts',
 89        { content, appearance_id: appearanceId },
 90        {
 91          headers,
 92          timeout: 30000,
 93        },
 94      )
 95  
 96      if (response.status === 200 || response.status === 201) {
 97        const result = response.data
 98        logForDebugging('Transcript shared successfully', { level: 'info' })
 99        return {
100          success: true,
101          transcriptId: result?.transcript_id,
102        }
103      }
104  
105      return { success: false }
106    } catch (err) {
107      logForDebugging(errorMessage(err), {
108        level: 'error',
109      })
110      return { success: false }
111    }
112  }