Files
zeekay 043ac58d3c chore: sweep upstream brand from ALL files; restore LICENSE/NOTICE attribution
Extends the sweep beyond packages/ to the whole repo (178 files): workflows,
.eslintrc, playground, examples, tooling, root docs. Zero upstream-brand refs and
zero such filenames outside the exemptions.

REVERTS A LICENSING ERROR (from earlier rebrand e43ab1af, not this sweep):
- LICENSE had its copyright holder rewritten to 'Insights'. Restored to the real
  holder: 'Copyright 2020 Posthog / Hiberly, Inc.' (+ Mixpanel, Inc.).
- NOTICE credited a non-existent upstream (github.com/getinsights/insights-js).
  Restored to the real one: github.com/PostHog/posthog-js.
Apache-2.0 4(c) requires retaining upstream attribution; rewriting a third
party's copyright line falsifies ownership. Both files are now excluded from the
guard and documented as exempt in LLM.md.

Also:
- Deleted junk: AGENTS.md~upstream_main (merge leftover), the now-obsolete
  rebrand script; renamed upload-*-s3.ts tooling (still used by cli.ts).
- Removed 42 stale playground/example pnpm-lock.yaml files: they pinned upstream
  packages the package.json no longer references. Regenerated on install —
  sed-ing a lockfile would corrupt its integrity hashes.
- Guard renamed no-posthog.yml -> brand-guard.yml and now assembles the pattern
  at runtime, so no filename or file content carries the string (the previous
  guard was swept by its own rule, inverting it to forbid 'insights').
2026-07-16 09:54:05 -07:00

340 lines
13 KiB
TypeScript

import { useEffect, useState } from 'react'
import { AppState, Button, ScrollView, StyleSheet, View } from 'react-native'
import { InsightsPersistedProperty } from 'insights-react-native'
import ParallaxScrollView from '@/components/ParallaxScrollView'
import { ThemedText } from '@/components/ThemedText'
import { ThemedView } from '@/components/ThemedView'
import { IconSymbol } from '@/components/ui/IconSymbol'
import { beforeSendMode, insights } from '../insights'
type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'
type BeforeSendMode = 'pass' | 'drop' | 'throw'
interface DevStatus {
distinctId: string | null
sessionId: string | null
appState: 'foreground' | 'background' | null
}
export default function LogsScreen() {
const [status, setStatus] = useState('Idle')
const [counter, setCounter] = useState(0)
const [devStatus, setDevStatus] = useState<DevStatus>({
distinctId: null,
sessionId: null,
appState: null,
})
// Mirror of `beforeSendMode.current` for UI display. The actual filter
// reads from the module-level ref in insights.tsx.
const [beforeSendModeUI, setBeforeSendModeUI] = useState<BeforeSendMode>(beforeSendMode.current)
const [queueDump, setQueueDump] = useState<string>('')
const refresh = (): void => {
setDevStatus({
distinctId: insights.getDistinctId() || null,
sessionId: insights.getSessionId() || null,
appState: ((insights as any)._currentAppState ?? null) as DevStatus['appState'],
})
}
// Emit a log on every AppState transition so the SDK's `app.state`
// tagging path can be exercised without manual taps. The Insights
// constructor registers its own AppState listener first, which updates
// `_currentAppState` synchronously *before* this hook's body runs, so
// the captured record reads the new state.
useEffect(() => {
let prev: string = AppState.currentState
const sub = AppState.addEventListener('change', (next) => {
insights.logger.info(`AppState ${prev} → ${next}`, { from: prev, to: next })
prev = next
refresh()
})
refresh()
return () => sub.remove()
}, [])
const bump = (message: string): void => {
const next = counter + 1
setCounter(next)
setStatus(`${message} (#${next} @ ${new Date().toISOString().slice(11, 19)})`)
}
// ===== Existing capture buttons =====
const sendOne = (level: LogLevel): void => {
insights.logger[level](`${level} log from example-expo-53`, {
source: 'logs-tab',
level,
tsClient: Date.now(),
})
bump(`Sent ${level}`)
}
const sendAllLevels = (): void => {
const levels: LogLevel[] = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']
levels.forEach((level, i) => {
insights.logger[level](`Level-sweep message (${level}) #${i}`, { sweep: true, index: i })
})
bump(`Sent all 6 levels`)
}
const sendStructured = (): void => {
insights.captureLog({
body: 'Structured attributes payload',
level: 'info',
attributes: {
userId: 'user-42',
feature: 'checkout',
durationMs: 123,
tags: ['beta', 'ios'],
nested: { foo: 'bar', count: 3 },
},
})
bump('Sent structured attrs')
}
const sendBurst = (): void => {
for (let i = 0; i < 20; i++) {
insights.logger[i % 2 === 0 ? 'info' : 'debug'](`Burst log #${i}`, { i })
}
bump('Sent 20-burst')
}
const sendFlood = (): void => {
// Hit the rate-cap window (default 500/10s on RN). Should emit
// 500-ish and then warn + drop.
for (let i = 0; i < 600; i++) {
insights.logger.info(`Flood #${i}`, { i, flood: true })
}
bump('Sent 600-flood (expect rate cap)')
}
const sendError = (): void => {
insights.logger.error('Simulated error with stack', {
errorName: 'SimulatedError',
errorMessage: 'Something bad happened',
stack: new Error('Simulated').stack ?? '',
})
bump('Sent error+stack')
}
const flushNow = async (): Promise<void> => {
try {
// Drain BOTH pipelines — events on `flush()`, logs on `flushLogs()`.
// Run in parallel so a slow events flush doesn't delay logs and
// vice-versa.
await Promise.all([insights.flush(), insights.flushLogs()])
bump('Manual flush ok')
} catch (e) {
setStatus(`Flush error: ${e}`)
}
}
// ===== Dev tools =====
//
// Status reads `_currentAppState` via a reach-in because there's no
// public getter for the SDK's cached AppState. Reach-ins like this are
// ONLY appropriate in dogfood-style examples; production wrappers
// should stick to the public surface (`captureLog`, `logger.*`,
// `flushLogs`).
const setBefore = (mode: BeforeSendMode): void => {
// Update the shared ref in insights.tsx — the `beforeSend` closure
// reads it on every capture, so behavior switches at runtime
// without touching SDK internals. This is the public-API-only
// pattern customers should follow for runtime-tunable filters.
beforeSendMode.current = mode
setBeforeSendModeUI(mode)
bump(`beforeSend = ${mode}`)
}
const captureNoLevel = (): void => {
// Verifies default-level INFO behaviour without a `level` arg.
insights.captureLog({ body: 'no-level test (defaults to INFO)' })
bump('Sent no-level (default INFO)')
}
const captureEmptyBody = (): void => {
insights.captureLog({ body: '' })
bump('Sent empty body (should be silently dropped)')
}
const captureNoBody = (): void => {
insights.captureLog({} as any)
bump('Sent no body (should be silently dropped)')
}
const dumpQueue = (): void => {
const queue = insights.getPersistedProperty(InsightsPersistedProperty.LogsQueue) as unknown[] | undefined
const text = queue
? `length=${queue.length}\n${JSON.stringify(queue, null, 2).slice(0, 4000)}`
: '(empty/undefined)'
setQueueDump(text)
}
const screenAndCapture = async (name: string): Promise<void> => {
await insights.screen(name)
insights.logger.info(`captured on ${name}`, { screenTagTest: true })
bump(`screen('${name}') + capture`)
}
const callIdentify = (id: string): void => {
insights.identify(id, { source: 'logs-tab' })
refresh()
bump(`identify('${id}')`)
}
const callReset = (): void => {
insights.reset()
refresh()
bump('reset()')
}
const callOptOut = async (): Promise<void> => {
await insights.optOut()
refresh()
bump('optOut()')
}
const callOptIn = async (): Promise<void> => {
await insights.optIn()
refresh()
bump('optIn()')
}
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#FFE6B3', dark: '#3D2E1D' }}
headerImage={<IconSymbol size={310} color="#808080" name="text.bubble.fill" style={styles.headerImage} />}
>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Logs</ThemedText>
</ThemedView>
<ThemedText>Status: {status}</ThemedText>
<ScrollView style={styles.scroll}>
<View style={styles.section}>
<ThemedText type="subtitle">Single level</ThemedText>
<View style={styles.row}>
<Button title="trace" onPress={() => sendOne('trace')} />
<Button title="debug" onPress={() => sendOne('debug')} />
<Button title="info" onPress={() => sendOne('info')} />
</View>
<View style={styles.row}>
<Button title="warn" onPress={() => sendOne('warn')} />
<Button title="error" onPress={() => sendOne('error')} />
<Button title="fatal" onPress={() => sendOne('fatal')} />
</View>
</View>
<View style={styles.section}>
<ThemedText type="subtitle">Mixed</ThemedText>
<Button title="All 6 levels" onPress={sendAllLevels} />
<Button title="Structured attributes" onPress={sendStructured} />
<Button title="Error + stack" onPress={sendError} />
</View>
<View style={styles.section}>
<ThemedText type="subtitle">Volume</ThemedText>
<Button title="Burst 20" onPress={sendBurst} />
<Button title="Flood 600 (rate cap)" onPress={sendFlood} />
</View>
<View style={styles.section}>
<ThemedText type="subtitle">Control</ThemedText>
<Button title="Flush now" onPress={flushNow} />
</View>
<View style={styles.divider} />
<ThemedText type="subtitle">Dev tools</ThemedText>
<View style={styles.section}>
<ThemedText type="defaultSemiBold">Status</ThemedText>
<ThemedText>distinctId: {devStatus.distinctId ?? '<none>'}</ThemedText>
<ThemedText>sessionId: {devStatus.sessionId ?? '<none>'}</ThemedText>
<ThemedText>app.state: {devStatus.appState ?? '<unknown>'}</ThemedText>
<ThemedText>beforeSend mode: {beforeSendModeUI}</ThemedText>
<Button title="Refresh status" onPress={refresh} />
</View>
<View style={styles.section}>
<ThemedText type="defaultSemiBold">Identity</ThemedText>
<View style={styles.row}>
<Button title="identify(alice)" onPress={() => callIdentify('alice')} />
<Button title="identify(bob)" onPress={() => callIdentify('bob')} />
</View>
<View style={styles.row}>
<Button title="reset()" onPress={callReset} />
<Button title="optOut()" onPress={callOptOut} />
<Button title="optIn()" onPress={callOptIn} />
</View>
</View>
<View style={styles.section}>
<ThemedText type="defaultSemiBold">Screen tagging</ThemedText>
<Button title="screen('Checkout') + capture" onPress={() => screenAndCapture('Checkout')} />
<Button title="screen('Profile') + capture" onPress={() => screenAndCapture('Profile')} />
</View>
<View style={styles.section}>
<ThemedText type="defaultSemiBold">Edge captures</ThemedText>
<Button title="captureLog (no level → INFO)" onPress={captureNoLevel} />
<Button title="captureLog (empty body)" onPress={captureEmptyBody} />
<Button title="captureLog (no body)" onPress={captureNoBody} />
</View>
<View style={styles.section}>
<ThemedText type="defaultSemiBold">beforeSend hook</ThemedText>
<View style={styles.row}>
<Button title="pass-through" onPress={() => setBefore('pass')} />
<Button title="drop (return null)" onPress={() => setBefore('drop')} />
<Button title="throw" onPress={() => setBefore('throw')} />
</View>
</View>
<View style={styles.section}>
<ThemedText type="defaultSemiBold">Storage</ThemedText>
<Button title="Dump LogsQueue" onPress={dumpQueue} />
<ScrollView style={styles.dump}>
<ThemedText>{queueDump || '(tap Dump to inspect)'}</ThemedText>
</ScrollView>
</View>
</ScrollView>
</ParallaxScrollView>
)
}
const styles = StyleSheet.create({
headerImage: {
color: '#808080',
bottom: -90,
left: -35,
position: 'absolute',
},
titleContainer: {
flexDirection: 'row',
gap: 8,
},
scroll: {
marginTop: 12,
},
section: {
marginBottom: 16,
gap: 6,
},
row: {
flexDirection: 'row',
gap: 8,
marginBottom: 6,
flexWrap: 'wrap',
},
divider: {
height: 1,
backgroundColor: 'rgba(128,128,128,0.4)',
marginVertical: 8,
},
dump: {
marginTop: 6,
maxHeight: 220,
backgroundColor: 'rgba(0,0,0,0.05)',
padding: 8,
},
})