Skip to content

Commit b7786da

Browse files
committed
Merge branch 'andrey/tile-compression' into andrey/no-jet-compose
* andrey/tile-compression: fix: Android span e2e tests (#397) match unit test chore: add CLAUDE.md (#398) chore: release main (#400) fix: correct react native session replay build step (#399)
2 parents b2fe94a + 91087e4 commit b7786da

12 files changed

Lines changed: 535 additions & 83 deletions

File tree

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"sdk/@launchdarkly/observability-node": "1.0.1",
77
"sdk/@launchdarkly/observability-python": "1.1.0",
88
"sdk/@launchdarkly/observability-react-native": "0.7.0",
9-
"sdk/@launchdarkly/react-native-ld-session-replay": "0.2.0",
9+
"sdk/@launchdarkly/react-native-ld-session-replay": "0.2.1",
1010
"sdk/@launchdarkly/session-replay": "1.0.3",
1111
"sdk/highlight-run": "9.27.1"
1212
}

CLAUDE.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# CLAUDE.md
2+
3+
## Overview
4+
5+
LaunchDarkly Observability SDK monorepo. Contains browser, Node.js, mobile, and backend SDKs for session replay, error monitoring, logging, and distributed tracing. Fork of highlight.io.
6+
7+
**Package manager:** Yarn 4.9.1 (`yarn install`)
8+
**Build system:** Turborepo
9+
**rrweb:** Git submodule at `rrweb/` - run `git submodule update --init --recursive` after clone (the preinstall hook handles this automatically).
10+
11+
## Key Commands
12+
13+
```bash
14+
yarn install # Install all dependencies
15+
yarn build # Build all packages (excludes rrweb, nextjs)
16+
yarn build:sdk # Build only @launchdarkly/* + highlight.run
17+
yarn test # Build, lint, enforce-size, then test all packages
18+
yarn format-check # Check formatting (prettier)
19+
yarn format:all # Fix formatting
20+
yarn lint # Lint all packages
21+
yarn enforce-size # Check bundle size limits (256KB brotli)
22+
yarn docs # Generate TypeDoc documentation
23+
```
24+
25+
### Filtering to specific packages
26+
27+
```bash
28+
yarn turbo run build --filter @launchdarkly/observability
29+
yarn turbo run test --filter highlight.run
30+
yarn turbo run build --filter '@launchdarkly/*'
31+
```
32+
33+
### Publishing (CI-only, runs on main)
34+
35+
```bash
36+
yarn publish # @launchdarkly/* packages
37+
yarn publish:highlight # highlight.run + @highlight-run/* packages
38+
```
39+
40+
## Package Structure
41+
42+
### LaunchDarkly SDKs (`sdk/@launchdarkly/`)
43+
44+
| Package | Description |
45+
|---------|-------------|
46+
| `observability` | Browser SDK - errors, logs, traces |
47+
| `session-replay` | Browser SDK - session replay (thin wrapper over highlight.run) |
48+
| `observability-shared` | Internal shared types and GraphQL codegen (private) |
49+
| `observability-node` | Node.js SDK |
50+
| `observability-react-native` | React Native SDK |
51+
| `react-native-ld-session-replay` | React Native session replay |
52+
| `observability-python` | Python SDK |
53+
| `observability-dotnet` | .NET SDK |
54+
| `observability-android` | Android SDK |
55+
| `observability-java` | Java SDK |
56+
57+
### Core packages
58+
59+
| Package | Description |
60+
|---------|-------------|
61+
| `sdk/highlight-run` | Core browser library - session replay + observability. Most logic lives here. |
62+
| `rrweb/` | Forked session replay recording library (git submodule, ~15 sub-packages) |
63+
| `go/` | Go observability SDK |
64+
65+
### Key source files in highlight.run
66+
67+
- `src/index.tsx` - Public API, `H` global singleton
68+
- `src/sdk/record.ts` - `RecordSDK` class (session replay recording logic)
69+
- `src/sdk/LDRecord.ts` - `LDRecord` global singleton (LaunchDarkly entry point)
70+
- `src/client/index.tsx` - `Highlight` class (underlying implementation)
71+
- `src/client/workers/highlight-client-worker.ts` - Web Worker for async data upload
72+
- `src/client/types/record.ts` - `RecordOptions` type definition
73+
- `src/client/types/iframe.ts` - Cross-origin iframe protocol types
74+
- `src/client/constants/sessions.ts` - Timing constants (`FIRST_SEND_FREQUENCY`, etc.)
75+
- `src/plugins/record.ts` - LaunchDarkly plugin interface (`LDPlugin`)
76+
77+
### Default backend URL
78+
79+
The SDK sends data to `https://pub.observability.app.launchdarkly.com` by default. Configurable via `backendUrl` option.
80+
81+
## Code Style
82+
83+
**Prettier config** (`.prettierrc`): Tabs, no semicolons, single quotes, trailing commas, 80 char width.
84+
85+
**Pre-commit hook** (husky): Runs `pretty-quick --staged` automatically.
86+
87+
**TypeScript**: Strict mode. All browser SDKs build with Vite (ESM + UMD). Node SDKs use rollup or tsc.
88+
89+
**Bundle size**: All browser packages enforce 256KB brotli limit via `size-limit`. Check with `yarn enforce-size`.
90+
91+
## CI Pipeline
92+
93+
The main workflow is `.github/workflows/turbo.yml` (runs on push to main and PRs):
94+
1. `yarn install`
95+
2. `yarn dedupe --check`
96+
3. `yarn format-check`
97+
4. `yarn test` (which runs build -> lint -> enforce-size -> test)
98+
5. On main: publishes to npm
99+
100+
Other workflows handle language-specific SDKs (Go, Python, .NET, Android, Java, Ruby, Rust, Elixir) and E2E tests.
101+
102+
Releases are managed by `release-please` - version bumps happen automatically via PR.
103+
104+
## E2E Tests
105+
106+
### Framework examples (`e2e/`)
107+
108+
Example apps for testing SDK integrations: React, Next.js, Angular, Express, NestJS, Remix, Hono, Cloudflare Worker, React Native, Go, Python, .NET, Ruby, and more.
109+
110+
```bash
111+
# Run via docker compose
112+
cd e2e
113+
docker compose build sdk && docker compose build base
114+
docker compose build <example> && docker compose up <example>
115+
116+
# Run specific frameworks via turbo
117+
yarn e2e:nextjs
118+
yarn e2e:express
119+
yarn e2e:cloudflare
120+
```
121+
122+
### Pytest E2E (`e2e/tests/`)
123+
124+
Python-based tests that start apps, make requests, and validate data appears in the backend via GraphQL queries. Uses `app_runner.py` for lifecycle management with health check polling.
125+
126+
```bash
127+
cd e2e/tests
128+
poetry install
129+
poetry run python src/app_runner.py <example>
130+
poetry run pytest
131+
```
132+
133+
**Run tests:**
134+
```bash
135+
# Diagnose live customer URLs (headed browser, slow-mo for observation)
136+
yarn test:live
137+
138+
# Run local reproduction tests (headless, uses local Express servers on :3001/:3002)
139+
yarn test:local
140+
141+
# Run everything
142+
yarn test
143+
```

e2e/react-three-vite/eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import reactHooks from 'eslint-plugin-react-hooks'
55
import reactRefresh from 'eslint-plugin-react-refresh'
66

77
export default [
8-
{ ignores: ['dist'] },
8+
{ ignores: ['dist', '**/*.timestamp-*'] },
99
{
1010
files: ['**/*.{js,jsx}'],
1111
languageOptions: {

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/ReplayOptions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ data class ReplayOptions(
2323
sealed class CompressionMethod {
2424
data object ScreenImage : CompressionMethod()
2525
data class OverlayTiles(
26-
val layers: Int = 10,
26+
val layers: Int = 15,
2727
val backtracking: Boolean = true,
2828
) : CompressionMethod()
2929
}

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/ExportDiffManager.kt

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,39 +46,35 @@ class ExportDiffManager(
4646
removes = currentImages.subList(lastKeyNodeIdx + 1, currentImages.size).toMutableList()
4747
currentImages.subList(lastKeyNodeIdx + 1, currentImages.size).clear()
4848

49-
// Keep only signatures that still point into the retained prefix.
5049
val filtered = currentImagesIndex.filterValues { value -> value <= lastKeyNodeIdx }
5150
currentImagesIndex.clear()
5251
currentImagesIndex.putAll(filtered)
5352
} else {
54-
for ((tileIdx, tile) in tiledFrame.tiles.withIndex()) {
55-
val tileSignature = signature.tileSignatures.getOrNull(tileIdx)
53+
for (tile in tiledFrame.tiles) {
5654
val addImage =
57-
tile.bitmap.asExportedImage(format = format, rect = tile.rect, tileSignature = tileSignature)
55+
tile.bitmap.asExportedImage(format = format, rect = tile.rect, imageSignature = signature)
5856
?: return null
5957
adds.add(addImage)
60-
if (tileSignature != null) {
61-
currentImages.add(
62-
ExportFrame.RemoveImage(
63-
keyFrameId = keyFrameId,
64-
tileSignature = tileSignature,
65-
)
58+
currentImages.add(
59+
ExportFrame.RemoveImage(
60+
keyFrameId = keyFrameId,
61+
imageSignature = signature,
6662
)
67-
}
63+
)
6864
}
6965
currentImagesIndex[signature] = currentImages.size - 1
7066
}
7167
} else {
72-
for ((tileIdx, tile) in tiledFrame.tiles.withIndex()) {
73-
val tileSignature = signature?.tileSignatures?.getOrNull(tileIdx)
74-
val addImage = tile.bitmap.asExportedImage(format = format, rect = tile.rect, tileSignature = tileSignature)
68+
for (tile in tiledFrame.tiles) {
69+
val imageSignature = tiledFrame.imageSignature
70+
val addImage = tile.bitmap.asExportedImage(format = format, rect = tile.rect, imageSignature = imageSignature)
7571
?: return null
7672
adds.add(addImage)
77-
if (tileSignature != null) {
73+
if (imageSignature != null) {
7874
currentImages.add(
7975
ExportFrame.RemoveImage(
8076
keyFrameId = keyFrameId,
81-
tileSignature = tileSignature,
77+
imageSignature = imageSignature,
8278
)
8379
)
8480
}
@@ -114,7 +110,7 @@ class ExportDiffManager(
114110
private fun Bitmap.asExportedImage(
115111
format: ExportFrame.ExportFormat,
116112
rect: IntRect,
117-
tileSignature: TileSignature?,
113+
imageSignature: ImageSignature?,
118114
): ExportFrame.AddImage? {
119115
val outputStream = ByteArrayOutputStream()
120116
return try {
@@ -138,7 +134,7 @@ private fun Bitmap.asExportedImage(
138134
ExportFrame.AddImage(
139135
imageBase64 = compressedImage,
140136
rect = rect,
141-
tileSignature = tileSignature,
137+
imageSignature = imageSignature,
142138
)
143139
} finally {
144140
outputStream.close()

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/ExportFrame.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ data class ExportFrame(
2828
AddImage(
2929
imageBase64 = imageBase64,
3030
rect = IntRect(left = 0, top = 0, width = origWidth, height = origHeight),
31-
tileSignature = null
31+
imageSignature = null
3232
)
3333
),
3434
removeImages = null,
@@ -44,13 +44,13 @@ data class ExportFrame(
4444

4545
data class RemoveImage(
4646
val keyFrameId: Int,
47-
val tileSignature: TileSignature,
47+
val imageSignature: ImageSignature,
4848
)
4949

5050
data class AddImage(
5151
val imageBase64: String,
5252
val rect: IntRect,
53-
val tileSignature: TileSignature?,
53+
val imageSignature: ImageSignature?,
5454
)
5555

5656
sealed class ExportFormat {

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/exporter/RRWebEventGenerator.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import com.launchdarkly.observability.replay.RRWebCustomDataTag
1515
import com.launchdarkly.observability.replay.RRWebIncrementalSource
1616
import com.launchdarkly.observability.replay.RRWebMouseInteraction
1717
import com.launchdarkly.observability.replay.capture.ExportFrame
18-
import com.launchdarkly.observability.replay.capture.TileSignature
18+
import com.launchdarkly.observability.replay.capture.ImageSignature
1919
import kotlinx.serialization.json.JsonPrimitive
2020
import kotlinx.serialization.json.buildJsonArray
2121
import kotlinx.serialization.json.buildJsonObject
@@ -57,7 +57,7 @@ class RRWebEventGenerator(
5757
private var imageNodeId: Int? = null
5858
private var bodyNodeId: Int? = null
5959
private var knownKeyFrameId: Int? = null
60-
private val nodeIds = mutableMapOf<TileSignature, Int>()
60+
private val nodeIds = mutableMapOf<ImageSignature, Int>()
6161

6262
data class State(
6363
val lastSid: Int = 0,
@@ -66,7 +66,7 @@ class RRWebEventGenerator(
6666
val imageNodeId: Int? = null,
6767
val bodyNodeId: Int? = null,
6868
val knownKeyFrameId: Int? = null,
69-
val nodeIds: Map<TileSignature, Int> = emptyMap(),
69+
val nodeIds: Map<ImageSignature, Int> = emptyMap(),
7070
)
7171

7272
private fun nextSid(): Int {
@@ -88,7 +88,7 @@ class RRWebEventGenerator(
8888

8989
private fun tileNode(exportFrame: ExportFrame, image: ExportFrame.AddImage): Pair<EventNode, Int> {
9090
val tileCanvasId = nextNodeId()
91-
image.tileSignature?.let { nodeIds[it] = tileCanvasId }
91+
image.imageSignature?.let { nodeIds[it] = tileCanvasId }
9292
val dataUrl = "data:${imageMimeType(exportFrame.format)};base64,${image.imageBase64}"
9393
val node = EventNode(
9494
id = tileCanvasId,
@@ -110,7 +110,7 @@ class RRWebEventGenerator(
110110

111111
var totalCanvasSize = 0
112112
val removes = exportFrame.removeImages?.mapNotNull { removal ->
113-
nodeIds[removal.tileSignature]?.let { nodeId ->
113+
nodeIds[removal.imageSignature]?.let { nodeId ->
114114
Removal(parentId = bodyId, id = nodeId)
115115
}
116116
} ?: emptyList()

0 commit comments

Comments
 (0)