mirror of
https://github.com/hanzoai/insights.git
synced 2026-11-03 00:52:32 +00:00
The rule table mapped HOG_ -> FN_ ahead of any POSTHOG rule, so POSTHOG_FOO split into POST + HOG_ and came out POSTFN_FOO. Nothing sets that. The live pods set INSIGHTS_SKIP_MIGRATION_CHECKS, which bin/migrate-check reads on the container entrypoint, so the misspelling does not degrade a feature -- it runs migrate --check against a database whose history this tree does not share and the container never starts. Both replacements are measured against the shipped tree rather than chosen: every HOG_ name with a counterpart there is spelled INSIGHTS_ (HOG_QL -> INSIGHTS_QL, HOG_FUNCTION_TEMPLATES -> INSIGHTS_FUNCTION_TEMPLATES, 12 of 12 determinate cases), and FN_ appears in it nowhere. Upstream carries 95 HOG_ tokens and zero FN_ ones, so repairing FN_ back out is exactly the inverse of the bad rule and cannot collide with an upstream name. The rule is fixed for the next convergence, and the tree it already produced is repaired in the same commit -- the codemod is idempotent against its own output, so it cannot undo this itself.
406 lines
15 KiB
TypeScript
406 lines
15 KiB
TypeScript
import { useActions, useValues } from 'kea'
|
|
import { useEffect } from 'react'
|
|
|
|
import { IconExternal } from '@hanzo/icons'
|
|
import { Button, Divider, Label, Select, Switch } from '@hanzo/elements'
|
|
|
|
import { CodeSnippet, Language } from 'lib/components/CodeSnippet'
|
|
import { superpowersLogic } from 'lib/components/Superpowers/superpowersLogic'
|
|
import { IconPlayCircle } from 'lib/elements/icons'
|
|
import { Field } from 'lib/elements/Field'
|
|
import { CodeEditorInline } from 'lib/monaco/CodeEditorInline'
|
|
import { urls } from 'scenes/urls'
|
|
|
|
import { SceneSection } from '~/layout/scenes/components/SceneSection'
|
|
import { EndpointVersionType } from '~/types'
|
|
|
|
import { CodeExampleTab, endpointLogic } from '../endpointLogic'
|
|
import { endpointSceneLogic, generateEndpointPayload } from '../endpointSceneLogic'
|
|
|
|
function formatPayloadForCodeExample(payload: Record<string, any>): string {
|
|
const entries = Object.entries(payload)
|
|
if (entries.length === 0) {
|
|
return ''
|
|
}
|
|
|
|
return entries
|
|
.map(([key, value], index) => {
|
|
const isLast = index === entries.length - 1
|
|
const comma = isLast ? '' : ','
|
|
|
|
// Format nested objects
|
|
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) {
|
|
return ` "${key}": {}${comma} // Add ${key} here`
|
|
}
|
|
|
|
// Format variables specially
|
|
if (key === 'variables' && typeof value === 'object') {
|
|
const varEntries = Object.entries(value)
|
|
if (varEntries.length === 0) {
|
|
return ` "${key}": {\n // No variables defined\n }${comma}`
|
|
}
|
|
const formattedVars = varEntries
|
|
.map(([varKey, varValue], varIndex) => {
|
|
const isLastVar = varIndex === varEntries.length - 1
|
|
const varComma = isLastVar ? '' : ','
|
|
return ` "${varKey}": ${JSON.stringify(varValue)}${varComma}`
|
|
})
|
|
.join('\n')
|
|
return ` "${key}": {\n${formattedVars}\n }${comma}`
|
|
}
|
|
|
|
return ` "${key}": ${JSON.stringify(value, null, 2).replace(/\n/g, '\n ')}${comma}`
|
|
})
|
|
.join('\n')
|
|
}
|
|
|
|
function getEndpointUrl(endpointPath: string): string {
|
|
return `${window.location.origin}${endpointPath}`
|
|
}
|
|
|
|
function generateTerminalExample(endpoint: EndpointVersionType, selectedVersion: number | null): string {
|
|
const payload = generateEndpointPayload(endpoint)
|
|
const hasPayload = Object.keys(payload).length > 0
|
|
const versionParam =
|
|
selectedVersion !== null && selectedVersion !== endpoint.current_version
|
|
? ` "version": ${selectedVersion}`
|
|
: ''
|
|
|
|
// If no payload and no version, omit the -d flag entirely
|
|
if (!hasPayload && !versionParam) {
|
|
return `curl -X POST ${getEndpointUrl(endpoint.endpoint_path)} \\
|
|
-H "Authorization: Bearer $INSIGHTS_PERSONAL_API_KEY"`
|
|
}
|
|
|
|
const payloadBody = formatPayloadForCodeExample(payload)
|
|
const dataContent = [payloadBody, versionParam].filter(Boolean).join(',\n')
|
|
|
|
return `curl -X POST ${getEndpointUrl(endpoint.endpoint_path)} \\
|
|
-H "Authorization: Bearer $INSIGHTS_PERSONAL_API_KEY" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{
|
|
${dataContent}
|
|
}'`
|
|
}
|
|
|
|
function generatePythonExample(endpoint: EndpointVersionType, selectedVersion: number | null): string {
|
|
const payload = generateEndpointPayload(endpoint)
|
|
const hasPayload = Object.keys(payload).length > 0
|
|
const versionParam =
|
|
selectedVersion !== null && selectedVersion !== endpoint.current_version
|
|
? ` "version": ${selectedVersion}`
|
|
: ''
|
|
|
|
// If no payload and no version, omit payload variable entirely
|
|
if (!hasPayload && !versionParam) {
|
|
return `import requests
|
|
|
|
url = "${getEndpointUrl(endpoint.endpoint_path)}"
|
|
|
|
headers = {
|
|
'Authorization': 'Bearer {INSIGHTS_PERSONAL_API_KEY}'
|
|
}
|
|
|
|
response = requests.post(url, headers=headers)
|
|
print(response.json())`
|
|
}
|
|
|
|
const payloadBody = formatPayloadForCodeExample(payload)
|
|
const dataContent = [payloadBody, versionParam].filter(Boolean).join(',\n')
|
|
|
|
return `import requests
|
|
import json
|
|
|
|
url = "${getEndpointUrl(endpoint.endpoint_path)}"
|
|
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer {INSIGHTS_PERSONAL_API_KEY}'
|
|
}
|
|
|
|
payload = {
|
|
${dataContent}
|
|
}
|
|
|
|
response = requests.post(url, headers=headers, data=json.dumps(payload))
|
|
print(response.json())`
|
|
}
|
|
|
|
function generateNodeExample(endpoint: EndpointVersionType, selectedVersion: number | null): string {
|
|
const payload = generateEndpointPayload(endpoint)
|
|
const hasPayload = Object.keys(payload).length > 0
|
|
const versionParam =
|
|
selectedVersion !== null && selectedVersion !== endpoint.current_version
|
|
? ` "version": ${selectedVersion}`
|
|
: ''
|
|
|
|
// If no payload and no version, omit payload variable and body entirely
|
|
if (!hasPayload && !versionParam) {
|
|
return `const fetch = require('node-fetch');
|
|
|
|
const url = '${getEndpointUrl(endpoint.endpoint_path)}';
|
|
|
|
const headers = {
|
|
'Authorization': 'Bearer {INSIGHTS_PERSONAL_API_KEY}'
|
|
};
|
|
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: headers
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => console.log(data))
|
|
.catch(error => console.error('Error:', error));`
|
|
}
|
|
|
|
const payloadBody = formatPayloadForCodeExample(payload)
|
|
const dataContent = [payloadBody, versionParam].filter(Boolean).join(',\n')
|
|
|
|
return `const fetch = require('node-fetch');
|
|
|
|
const url = '${getEndpointUrl(endpoint.endpoint_path)}';
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer {INSIGHTS_PERSONAL_API_KEY}'
|
|
};
|
|
|
|
const payload = {
|
|
${dataContent}
|
|
};
|
|
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => console.log(data))
|
|
.catch(error => console.error('Error:', error));`
|
|
}
|
|
|
|
export function EndpointPlayground(): JSX.Element {
|
|
const { endpoint } = useValues(endpointLogic)
|
|
const { payloadJson, payloadJsonError, endpointResult, endpointResultLoading, viewingVersion, debugMode } =
|
|
useValues(endpointSceneLogic)
|
|
const { setPayloadJson, setPayloadJsonError, loadEndpointResult, setDebugMode } = useActions(endpointSceneLogic)
|
|
const { setActiveCodeExampleTab, setSelectedCodeExampleVersion } = useActions(endpointLogic)
|
|
const { activeCodeExampleTab, selectedCodeExampleVersion } = useValues(endpointLogic)
|
|
const { superpowersEnabled } = useValues(superpowersLogic)
|
|
|
|
// When viewing a specific version, use that version for code examples
|
|
const effectiveVersion = viewingVersion?.version ?? selectedCodeExampleVersion
|
|
|
|
const handleExecute = (): void => {
|
|
if (!endpoint?.name) {
|
|
return
|
|
}
|
|
|
|
let data: any = {}
|
|
try {
|
|
data = payloadJson && payloadJson.trim() !== '' ? JSON.parse(payloadJson) : {}
|
|
} catch {
|
|
setPayloadJsonError('Invalid JSON in request payload')
|
|
return
|
|
}
|
|
|
|
if (debugMode) {
|
|
data = { ...data, debug: true }
|
|
}
|
|
|
|
loadEndpointResult({ name: endpoint.name, data })
|
|
}
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent): void => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
handleExecute()
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', handleKeyDown, true)
|
|
return () => window.removeEventListener('keydown', handleKeyDown, true)
|
|
}, [endpoint?.name, payloadJson, handleExecute])
|
|
|
|
if (!endpoint) {
|
|
return <></>
|
|
}
|
|
|
|
const getCodeExample = (tab: CodeExampleTab): string => {
|
|
switch (tab) {
|
|
case 'terminal':
|
|
return generateTerminalExample(endpoint, effectiveVersion)
|
|
case 'python':
|
|
return generatePythonExample(endpoint, effectiveVersion)
|
|
case 'nodejs':
|
|
return generateNodeExample(endpoint, effectiveVersion)
|
|
default:
|
|
return generateTerminalExample(endpoint, effectiveVersion)
|
|
}
|
|
}
|
|
|
|
const getLanguage = (tab: CodeExampleTab): Language => {
|
|
switch (tab) {
|
|
case 'terminal':
|
|
return Language.Bash
|
|
case 'python':
|
|
return Language.Python
|
|
case 'nodejs':
|
|
return Language.JavaScript
|
|
default:
|
|
return Language.Bash
|
|
}
|
|
}
|
|
|
|
// Generate version options
|
|
const versionOptions = Array.from({ length: endpoint.current_version }, (_, i) => {
|
|
const version = i + 1
|
|
return {
|
|
value: version,
|
|
label: version === endpoint.current_version ? `v${version} (Current)` : `v${version}`,
|
|
}
|
|
})
|
|
|
|
return (
|
|
<SceneSection
|
|
title="Playground"
|
|
description={
|
|
<>
|
|
Send API requests to your endpoints, play with setting different parameters in the request body and
|
|
see what the resulting JSON response would look like. <br />
|
|
Once you're done experimenting, find the code snippet for your use case below.
|
|
</>
|
|
}
|
|
>
|
|
<div className="flex gap-4" data-attr="endpoint-playground">
|
|
<div className="flex-1 flex flex-col gap-2">
|
|
<Field.Pure
|
|
label="Request payload"
|
|
info={
|
|
<>
|
|
JSON payload sent with the request. Use <code className="text-xs">"variables"</code> to
|
|
pass query parameters.
|
|
</>
|
|
}
|
|
/>
|
|
|
|
<CodeEditorInline
|
|
embedded
|
|
language="json"
|
|
value={payloadJson}
|
|
onChange={(value) => setPayloadJson(value ?? '')}
|
|
maxHeight={400}
|
|
/>
|
|
{payloadJsonError && <Field.Pure error={payloadJsonError} />}
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
icon={<IconPlayCircle />}
|
|
onClick={handleExecute}
|
|
loading={endpointResultLoading}
|
|
tooltip="Cmd/Ctrl + Enter"
|
|
disabledReason={
|
|
!endpoint?.is_active
|
|
? 'This endpoint is inactive. Activate it in the actions panel on the top right to execute.'
|
|
: undefined
|
|
}
|
|
>
|
|
Execute endpoint
|
|
</Button>
|
|
{superpowersEnabled && (
|
|
<Switch checked={debugMode} onChange={setDebugMode} label="Debug" bordered />
|
|
)}
|
|
</div>
|
|
{endpointResult &&
|
|
!endpointResultLoading &&
|
|
(() => {
|
|
try {
|
|
const parsed = JSON.parse(endpointResult)
|
|
if (
|
|
'results' in parsed &&
|
|
Array.isArray(parsed.results) &&
|
|
parsed.results.length === 0
|
|
) {
|
|
return <Field.Pure error="No results" />
|
|
}
|
|
} catch {
|
|
// Invalid JSON, don't show anything
|
|
}
|
|
return null
|
|
})()}
|
|
</div>
|
|
|
|
<div className="flex-3 flex flex-col gap-2">
|
|
<Field.Pure
|
|
label="API response"
|
|
info={
|
|
<>
|
|
API response from the endpoint. Pay attention to the{' '}
|
|
<code className="text-xs">"results"</code> key in the JSON below.
|
|
</>
|
|
}
|
|
/>
|
|
<CodeEditorInline
|
|
embedded
|
|
language="json"
|
|
value={endpointResult || 'Execute endpoint to see API response.'}
|
|
maxHeight={400}
|
|
options={{
|
|
readOnly: true,
|
|
lineNumbers: 'on',
|
|
folding: true,
|
|
foldingStrategy: 'indentation',
|
|
showFoldingControls: 'always',
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<Divider className="my-4" />
|
|
<div className="flex flex-col gap-4">
|
|
<Label info="Create a personal API key and copy a code example to call this endpoint from your application.">
|
|
Example usage
|
|
</Label>
|
|
<div className="flex gap-2">
|
|
<Select
|
|
options={versionOptions}
|
|
onChange={setSelectedCodeExampleVersion}
|
|
value={effectiveVersion || endpoint.current_version}
|
|
placeholder="Select version"
|
|
/>
|
|
<Select
|
|
options={[
|
|
{ value: 'terminal', label: 'Terminal' },
|
|
{ value: 'python', label: 'Python' },
|
|
{ value: 'nodejs', label: 'Node.js' },
|
|
]}
|
|
onChange={(val) => {
|
|
if (val) {
|
|
setActiveCodeExampleTab(val as CodeExampleTab)
|
|
}
|
|
}}
|
|
value={activeCodeExampleTab}
|
|
/>
|
|
<Button
|
|
to={urls.settings('user', 'personal-api-keys')}
|
|
type="secondary"
|
|
size="small"
|
|
icon={<IconExternal />}
|
|
targetBlank
|
|
>
|
|
Personal API keys
|
|
</Button>
|
|
</div>
|
|
<div>
|
|
<CodeSnippet language={getLanguage(activeCodeExampleTab)} wrap={true}>
|
|
{getCodeExample(activeCodeExampleTab)}
|
|
</CodeSnippet>
|
|
</div>
|
|
</div>
|
|
</SceneSection>
|
|
)
|
|
}
|