mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 12:56:58 +10:00
296 lines
47 KiB
TypeScript
296 lines
47 KiB
TypeScript
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||
|
|
import { useDynamicConfig } from 'src/hooks/useDynamicConfig.js';
|
||
|
|
import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js';
|
||
|
|
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||
|
|
import { isPolicyAllowed } from '../../services/policyLimits/index.js';
|
||
|
|
import type { Message } from '../../types/message.js';
|
||
|
|
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||
|
|
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||
|
|
import { getLastAssistantMessage } from '../../utils/messages.js';
|
||
|
|
import { getMainLoopModel } from '../../utils/model/model.js';
|
||
|
|
import { getInitialSettings } from '../../utils/settings/settings.js';
|
||
|
|
import { logOTelEvent } from '../../utils/telemetry/events.js';
|
||
|
|
import { submitTranscriptShare, type TranscriptShareTrigger } from './submitTranscriptShare.js';
|
||
|
|
import type { TranscriptShareResponse } from './TranscriptSharePrompt.js';
|
||
|
|
import { useSurveyState } from './useSurveyState.js';
|
||
|
|
import type { FeedbackSurveyResponse, FeedbackSurveyType } from './utils.js';
|
||
|
|
type FeedbackSurveyConfig = {
|
||
|
|
minTimeBeforeFeedbackMs: number;
|
||
|
|
minTimeBetweenFeedbackMs: number;
|
||
|
|
minTimeBetweenGlobalFeedbackMs: number;
|
||
|
|
minUserTurnsBeforeFeedback: number;
|
||
|
|
minUserTurnsBetweenFeedback: number;
|
||
|
|
hideThanksAfterMs: number;
|
||
|
|
onForModels: string[];
|
||
|
|
probability: number;
|
||
|
|
};
|
||
|
|
type TranscriptAskConfig = {
|
||
|
|
probability: number;
|
||
|
|
};
|
||
|
|
const DEFAULT_FEEDBACK_SURVEY_CONFIG: FeedbackSurveyConfig = {
|
||
|
|
minTimeBeforeFeedbackMs: 600000,
|
||
|
|
minTimeBetweenFeedbackMs: 3600000,
|
||
|
|
minTimeBetweenGlobalFeedbackMs: 100000000,
|
||
|
|
minUserTurnsBeforeFeedback: 5,
|
||
|
|
minUserTurnsBetweenFeedback: 10,
|
||
|
|
hideThanksAfterMs: 3000,
|
||
|
|
onForModels: ['*'],
|
||
|
|
probability: 0.005
|
||
|
|
};
|
||
|
|
const DEFAULT_TRANSCRIPT_ASK_CONFIG: TranscriptAskConfig = {
|
||
|
|
probability: 0
|
||
|
|
};
|
||
|
|
export function useFeedbackSurvey(messages: Message[], isLoading: boolean, submitCount: number, surveyType: FeedbackSurveyType = 'session', hasActivePrompt: boolean = false): {
|
||
|
|
state: 'closed' | 'open' | 'thanks' | 'transcript_prompt' | 'submitting' | 'submitted';
|
||
|
|
lastResponse: FeedbackSurveyResponse | null;
|
||
|
|
handleSelect: (selected: FeedbackSurveyResponse) => boolean;
|
||
|
|
handleTranscriptSelect: (selected: TranscriptShareResponse) => void;
|
||
|
|
} {
|
||
|
|
const lastAssistantMessageIdRef = useRef('unknown');
|
||
|
|
lastAssistantMessageIdRef.current = getLastAssistantMessage(messages)?.message?.id || 'unknown';
|
||
|
|
const [feedbackSurvey, setFeedbackSurvey] = useState<{
|
||
|
|
timeLastShown: number | null;
|
||
|
|
submitCountAtLastAppearance: number | null;
|
||
|
|
}>(() => ({
|
||
|
|
timeLastShown: null,
|
||
|
|
submitCountAtLastAppearance: null
|
||
|
|
}));
|
||
|
|
const config = useDynamicConfig<FeedbackSurveyConfig>('tengu_feedback_survey_config', DEFAULT_FEEDBACK_SURVEY_CONFIG);
|
||
|
|
const badTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>('tengu_bad_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG);
|
||
|
|
const goodTranscriptAskConfig = useDynamicConfig<TranscriptAskConfig>('tengu_good_survey_transcript_ask_config', DEFAULT_TRANSCRIPT_ASK_CONFIG);
|
||
|
|
const settingsRate = getInitialSettings().feedbackSurveyRate;
|
||
|
|
const sessionStartTime = useRef(Date.now());
|
||
|
|
const submitCountAtSessionStart = useRef(submitCount);
|
||
|
|
const submitCountRef = useRef(submitCount);
|
||
|
|
submitCountRef.current = submitCount;
|
||
|
|
const messagesRef = useRef(messages);
|
||
|
|
messagesRef.current = messages;
|
||
|
|
// Probability gate: roll once when eligibility conditions are met, not on every
|
||
|
|
// useMemo re-evaluation. Without this, each dependency change (submitCount,
|
||
|
|
// isLoading toggle, etc.) re-rolls Math.random(), making the survey almost
|
||
|
|
// certain to appear after enough renders.
|
||
|
|
const probabilityPassedRef = useRef(false);
|
||
|
|
const lastEligibleSubmitCountRef = useRef<number | null>(null);
|
||
|
|
const updateLastShownTime = useCallback((timestamp: number, submitCountValue: number) => {
|
||
|
|
setFeedbackSurvey(prev => {
|
||
|
|
if (prev.timeLastShown === timestamp && prev.submitCountAtLastAppearance === submitCountValue) {
|
||
|
|
return prev;
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
timeLastShown: timestamp,
|
||
|
|
submitCountAtLastAppearance: submitCountValue
|
||
|
|
};
|
||
|
|
});
|
||
|
|
// Persist cross-session pacing state (previously done by onChangeAppState observer)
|
||
|
|
if (getGlobalConfig().feedbackSurveyState?.lastShownTime !== timestamp) {
|
||
|
|
saveGlobalConfig(current => ({
|
||
|
|
...current,
|
||
|
|
feedbackSurveyState: {
|
||
|
|
lastShownTime: timestamp
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
const onOpen = useCallback((appearanceId: string) => {
|
||
|
|
updateLastShownTime(Date.now(), submitCountRef.current);
|
||
|
|
logEvent('tengu_feedback_survey_event', {
|
||
|
|
event_type: 'appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
appearance_id: appearanceId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||
|
|
});
|
||
|
|
void logOTelEvent('feedback_survey', {
|
||
|
|
event_type: 'appeared',
|
||
|
|
appearance_id: appearanceId,
|
||
|
|
survey_type: surveyType
|
||
|
|
});
|
||
|
|
}, [updateLastShownTime, surveyType]);
|
||
|
|
const onSelect = useCallback((appearanceId_0: string, selected: FeedbackSurveyResponse) => {
|
||
|
|
updateLastShownTime(Date.now(), submitCountRef.current);
|
||
|
|
logEvent('tengu_feedback_survey_event', {
|
||
|
|
event_type: 'responded' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
appearance_id: appearanceId_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
response: selected as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||
|
|
});
|
||
|
|
void logOTelEvent('feedback_survey', {
|
||
|
|
event_type: 'responded',
|
||
|
|
appearance_id: appearanceId_0,
|
||
|
|
response: selected,
|
||
|
|
survey_type: surveyType
|
||
|
|
});
|
||
|
|
}, [updateLastShownTime, surveyType]);
|
||
|
|
const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => {
|
||
|
|
// Only bad and good ratings trigger the transcript ask
|
||
|
|
if (selected_0 !== 'bad' && selected_0 !== 'good') {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Don't show if user previously chose "Don't ask again"
|
||
|
|
if (getGlobalConfig().transcriptShareDismissed) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Don't show if product feedback is blocked by org policy (ZDR)
|
||
|
|
if (!isPolicyAllowed('allow_product_feedback')) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Probability gate from GrowthBook config (separate per rating)
|
||
|
|
const probability = selected_0 === 'bad' ? badTranscriptAskConfig.probability : goodTranscriptAskConfig.probability;
|
||
|
|
return Math.random() <= probability;
|
||
|
|
}, [badTranscriptAskConfig.probability, goodTranscriptAskConfig.probability]);
|
||
|
|
const onTranscriptPromptShown = useCallback((appearanceId_1: string, surveyResponse: FeedbackSurveyResponse) => {
|
||
|
|
const trigger: TranscriptShareTrigger = surveyResponse === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey';
|
||
|
|
logEvent('tengu_feedback_survey_event', {
|
||
|
|
event_type: 'transcript_prompt_appeared' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
appearance_id: appearanceId_1 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
trigger: trigger as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||
|
|
});
|
||
|
|
void logOTelEvent('feedback_survey', {
|
||
|
|
event_type: 'transcript_prompt_appeared',
|
||
|
|
appearance_id: appearanceId_1,
|
||
|
|
survey_type: surveyType
|
||
|
|
});
|
||
|
|
}, [surveyType]);
|
||
|
|
const onTranscriptSelect = useCallback(async (appearanceId_2: string, selected_1: TranscriptShareResponse, surveyResponse_0: FeedbackSurveyResponse | null): Promise<boolean> => {
|
||
|
|
const trigger_0: TranscriptShareTrigger = surveyResponse_0 === 'good' ? 'good_feedback_survey' : 'bad_feedback_survey';
|
||
|
|
logEvent('tengu_feedback_survey_event', {
|
||
|
|
event_type: `transcript_share_${selected_1}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
last_assistant_message_id: lastAssistantMessageIdRef.current as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
survey_type: surveyType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||
|
|
});
|
||
|
|
if (selected_1 === 'dont_ask_again') {
|
||
|
|
saveGlobalConfig(current_0 => ({
|
||
|
|
...current_0,
|
||
|
|
transcriptShareDismissed: true
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
if (selected_1 === 'yes') {
|
||
|
|
const result = await submitTranscriptShare(messagesRef.current, trigger_0, appearanceId_2);
|
||
|
|
logEvent('tengu_feedback_survey_event', {
|
||
|
|
event_type: (result.success ? 'transcript_share_submitted' : 'transcript_share_failed') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
appearance_id: appearanceId_2 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||
|
|
trigger: trigger_0 as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||
|
|
});
|
||
|
|
return result.success;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}, [surveyType]);
|
||
|
|
const {
|
||
|
|
state,
|
||
|
|
lastResponse,
|
||
|
|
open,
|
||
|
|
handleSelect,
|
||
|
|
handleTranscriptSelect
|
||
|
|
} = useSurveyState({
|
||
|
|
hideThanksAfterMs: config.hideThanksAfterMs,
|
||
|
|
onOpen,
|
||
|
|
onSelect,
|
||
|
|
shouldShowTranscriptPrompt,
|
||
|
|
onTranscriptPromptShown,
|
||
|
|
onTranscriptSelect
|
||
|
|
});
|
||
|
|
const currentModel = getMainLoopModel();
|
||
|
|
const isModelAllowed = useMemo(() => {
|
||
|
|
if (config.onForModels.length === 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (config.onForModels.includes('*')) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return config.onForModels.includes(currentModel);
|
||
|
|
}, [config.onForModels, currentModel]);
|
||
|
|
const shouldOpen = useMemo(() => {
|
||
|
|
if (state !== 'closed') {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (isLoading) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Don't show survey when permission or ask question prompts are visible
|
||
|
|
if (hasActivePrompt) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Force display for testing
|
||
|
|
if (process.env.CLAUDE_FORCE_DISPLAY_SURVEY && !feedbackSurvey.timeLastShown) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (!isModelAllowed) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (isFeedbackSurveyDisabled()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if product feedback is allowed by org policy
|
||
|
|
if (!isPolicyAllowed('allow_product_feedback')) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check session-local pacing
|
||
|
|
if (feedbackSurvey.timeLastShown) {
|
||
|
|
// Check time elapsed since last appearance in this session
|
||
|
|
const timeSinceLastShown = Date.now() - feedbackSurvey.timeLastShown;
|
||
|
|
if (timeSinceLastShown < config.minTimeBetweenFeedbackMs) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
// Check user turn requirement for subsequent appearances
|
||
|
|
if (feedbackSurvey.submitCountAtLastAppearance !== null && submitCount < feedbackSurvey.submitCountAtLastAppearance + config.minUserTurnsBetweenFeedback) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// First appearance in this session
|
||
|
|
const timeSinceSessionStart = Date.now() - sessionStartTime.current;
|
||
|
|
if (timeSinceSessionStart < config.minTimeBeforeFeedbackMs) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (submitCount < submitCountAtSessionStart.current + config.minUserTurnsBeforeFeedback) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Probability check: roll once per eligibility window to avoid re-rolling
|
||
|
|
// on every useMemo re-evaluation (which would make triggering near-certain).
|
||
|
|
if (lastEligibleSubmitCountRef.current !== submitCount) {
|
||
|
|
lastEligibleSubmitCountRef.current = submitCount;
|
||
|
|
probabilityPassedRef.current = Math.random() <= (settingsRate ?? config.probability);
|
||
|
|
}
|
||
|
|
if (!probabilityPassedRef.current) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check global pacing (across all sessions)
|
||
|
|
// Leave this till last because it reads from the filesystem which is expensive.
|
||
|
|
const globalFeedbackState = getGlobalConfig().feedbackSurveyState;
|
||
|
|
if (globalFeedbackState?.lastShownTime) {
|
||
|
|
const timeSinceGlobalLastShown = Date.now() - globalFeedbackState.lastShownTime;
|
||
|
|
if (timeSinceGlobalLastShown < config.minTimeBetweenGlobalFeedbackMs) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}, [state, isLoading, hasActivePrompt, isModelAllowed, feedbackSurvey.timeLastShown, feedbackSurvey.submitCountAtLastAppearance, submitCount, config.minTimeBetweenFeedbackMs, config.minTimeBetweenGlobalFeedbackMs, config.minUserTurnsBetweenFeedback, config.minTimeBeforeFeedbackMs, config.minUserTurnsBeforeFeedback, config.probability, settingsRate]);
|
||
|
|
useEffect(() => {
|
||
|
|
if (shouldOpen) {
|
||
|
|
open();
|
||
|
|
}
|
||
|
|
}, [shouldOpen, open]);
|
||
|
|
return {
|
||
|
|
state,
|
||
|
|
lastResponse,
|
||
|
|
handleSelect,
|
||
|
|
handleTranscriptSelect
|
||
|
|
};
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJ1c2VDYWxsYmFjayIsInVzZUVmZmVjdCIsInVzZU1lbW8iLCJ1c2VSZWYiLCJ1c2VTdGF0ZSIsInVzZUR5bmFtaWNDb25maWciLCJpc0ZlZWRiYWNrU3VydmV5RGlzYWJsZWQiLCJBbmFseXRpY3NNZXRhZGF0YV9JX1ZFUklGSUVEX1RISVNfSVNfTk9UX0NPREVfT1JfRklMRVBBVEhTIiwibG9nRXZlbnQiLCJpc1BvbGljeUFsbG93ZWQiLCJNZXNzYWdlIiwiZ2V0R2xvYmFsQ29uZmlnIiwic2F2ZUdsb2JhbENvbmZpZyIsImlzRW52VHJ1dGh5IiwiZ2V0TGFzdEFzc2lzdGFudE1lc3NhZ2UiLCJnZXRNYWluTG9vcE1vZGVsIiwiZ2V0SW5pdGlhbFNldHRpbmdzIiwibG9nT1RlbEV2ZW50Iiwic3VibWl0VHJhbnNjcmlwdFNoYXJlIiwiVHJhbnNjcmlwdFNoYXJlVHJpZ2dlciIsIlRyYW5zY3JpcHRTaGFyZVJlc3BvbnNlIiwidXNlU3VydmV5U3RhdGUiLCJGZWVkYmFja1N1cnZleVJlc3BvbnNlIiwiRmVlZGJhY2tTdXJ2ZXlUeXBlIiwiRmVlZGJhY2tTdXJ2ZXlDb25maWciLCJtaW5UaW1lQmVmb3JlRmVlZGJhY2tNcyIsIm1pblRpbWVCZXR3ZWVuRmVlZGJhY2tNcyIsIm1pblRpbWVCZXR3ZWVuR2xvYmFsRmVlZGJhY2tNcyIsIm1pblVzZXJUdXJuc0JlZm9yZUZlZWRiYWNrIiwibWluVXNlclR1cm5zQmV0d2VlbkZlZWRiYWNrIiwiaGlkZVRoYW5rc0FmdGVyTXMiLCJvbkZvck1vZGVscyIsInByb2JhYmlsaXR5IiwiVHJhbnNjcmlwdEFza0NvbmZpZyIsIkRFRkFVTFRfRkVFREJBQ0tfU1VSVkVZX0NPTkZJRyIsIkRFRkFVTFRfVFJBTlNDUklQVF9BU0tfQ09ORklHIiwidXNlRmVlZGJhY2tTdXJ2ZXkiLCJtZXNzYWdlcyIsImlzTG9hZGluZyIsInN1Ym1pdENvdW50Iiwic3VydmV5VHlwZSIsImhhc0FjdGl2ZVByb21wdCIsInN0YXRlIiwibGFzdFJlc3BvbnNlIiwiaGFuZGxlU2VsZWN0Iiwic2VsZWN0ZWQiLCJoYW5kbGVUcmFuc2NyaXB0U2VsZWN0IiwibGFzdEFzc2lzdGFudE1lc3NhZ2VJZFJlZiIsImN1cnJlbnQiLCJtZXNzYWdlIiwiaWQiLCJmZWVkYmFja1N1cnZleSIsInNldEZlZWRiYWNrU3VydmV5IiwidGltZUxhc3RTaG93biIsInN1Ym1pdENvdW50QXRMYXN0QXBwZWFyYW5jZSIsImNvbmZpZyIsImJhZFRyYW5zY3JpcHRBc2tDb25maWciLCJnb29kVHJhbnNjcmlwdEFza0NvbmZpZyIsInNldHRpbmdzUmF0ZSIsImZlZWRiYWNrU3VydmV5UmF0ZSIsInNlc3Npb25TdGFydFRpbWUiLCJEYXRlIiwibm93Iiwic3VibWl0Q291bnRBdFNlc3Npb25TdGFydCIsInN1Ym1pdENvdW50UmVmIiwibWVzc2FnZXNSZWYiLCJwcm9iYWJpbGl0eVBhc3NlZFJlZiIsImxhc3RFbGlnaWJsZVN1Ym1pdENvdW50UmVmIiwidXBkYXRlTGFzdFNob3duVGltZSIsInRpbWVzdGFtcCIsInN1Ym1pdENvdW50VmFsdWUiLCJwcmV2IiwiZmVlZGJhY2tTdXJ2ZXlTdGF0ZSIsImxhc3RTaG93blRpbWUiLCJvbk9wZW4iLCJhcHBlYXJhbmNlSWQiLCJldmVudF90eXBlIiwiYXBwZWFyYW5jZV9pZCIsImxhc3RfYXNzaXN0YW50X21lc3NhZ2VfaWQiLCJzdXJ2ZXlfdHlwZSIsIm9uU2VsZWN0IiwicmVzcG9uc2UiLCJzaG91bGRTaG93VHJhbnNjcmlwdFByb21wdCIsInRyYW5zY3JpcHRTaGFyZURpc21pc3NlZCIsIk1hdGgiLCJyYW5kb20iLCJvblRyYW5zY3JpcHRQcm9tcHRTaG93biIsInN1cnZleVJlc3BvbnNlIiwidHJpZ2dlciIsIm9uVHJhbnNjcmlwdFNlbGVjdCIsIlByb21pc2UiLCJyZXN1bHQiLCJzdWNjZXNzIiwib3BlbiIsImN1cnJlbnRNb2RlbCIsImlzTW9kZWxBbGxvd2VkIiwibGVuZ3RoIiwiaW5jbHVkZXMiLCJzaG91bGRPcGVuIiwicHJvY2VzcyIsImVudiIsIkNMQVVERV9GT1JDRV9ESVNQTEFZX1NVUlZFWSIsIkNMQVVERV9DT0RFX0RJU0FCTEVfRkVFREJBQ0tfU1VSVkVZIiwidGltZVNpbmNlTGFzdFNob3duIiwidGltZVNpbmNlU2Vzc2lvblN0YXJ0IiwiZ2xvYmFsRmVlZGJhY2tTdGF0ZSIsInRpbWVTaW5jZUdsb2JhbExhc3RTaG93biJdLCJzb3VyY2VzIjpbInVzZUZlZWRiYWNrU3VydmV5LnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyB1c2VDYWxsYmFjaywgdXNlRWZmZWN0LCB1c2VNZW1vLCB1c2VSZWYsIHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VEeW5hbWljQ29uZmlnIH0gZnJvbSAnc3JjL2hvb2tzL3VzZUR5bmFtaWNDb25maWcuanMnXG5pbXBvcnQgeyBpc0ZlZWRiYWNrU3VydmV5RGlzYWJsZWQgfSBmcm9tICdzcmMvc2VydmljZXMvYW5hbHl0aWNzL2NvbmZpZy5qcydcbmltcG9ydCB7XG4gIHR5cGUgQW5hbHl0aWNzTWV0YWRhdGFfSV9WRVJJRklFRF9USElTX0lTX05PVF9DT0RFX09SX0ZJTEVQQVRIUyxcbiAgbG9nRXZlbnQsXG59IGZyb20gJ3NyYy9zZXJ2aWNlcy9hbmFseXRpY3MvaW5kZXguanMnXG5pbXBvcnQgeyBpc1BvbGljeUFsbG93ZWQgfSBmcm9tICcuLi8uLi9zZXJ2aWNlcy9wb2xpY3lMaW1pdHMvaW5kZXguanMnXG5pbXBvcnQgdHlwZSB7IE1lc3NhZ2UgfSBmcm9tICcuLi8uLi90eXBlcy9tZXNzYWdlLmpzJ1xuaW1wb3J0IHsgZ2V0R2xvYmFsQ29uZmlnLCBzYXZlR2xvYmFsQ29uZmlnIH0gZnJvbSAnLi4vLi4vdXRpbHMvY29uZmlnLmpzJ1xuaW1wb3J0IHsgaXNFbnZUcnV0aHkgfSBmcm9tICcuLi8uLi91dGlscy9lbnZVdGlscy5qcydcbmltcG9ydCB7IGdldExhc3RBc3Npc3RhbnRNZXNzYWdlIH0gZnJvbSAnLi4vLi4vdXRpbHMvbWVzc2FnZXMuanMnXG5pbXBvcnQgeyBnZXRNYWluTG9vcE1vZGVsIH0gZnJvbSAnLi4vLi4vdXRpbHMvbW9kZWwvbW9kZWwuanMnXG5pbXBvcnQgeyBnZXRJbml0aWFsU2V0dGluZ3MgfSBmcm9tICcuLi8uLi91dGlscy9zZXR0aW5ncy9zZXR0aW5ncy5qcydcbmltcG9ydCB7IGxvZ09UZWxFdmVudCB9IGZyb20gJy4uLy4uL3V0aWxzL3RlbGVtZXRyeS9ldmVudHMuanMnXG5pbXBvcnQge1xuICBzdWJtaXRUcmFuc2NyaXB0U2hhcmUsXG4gIHR5cGUgVHJhbnNjcmlwdFNoYXJlVHJpZ2dlcixcbn0gZnJvbSAnLi9zdWJtaXRUcmFuc2NyaXB0U2hhcmUuanMnXG5pbXBvcnQgdHlwZSB7IFR
|