Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/router-e2e/__e2e__/native-navigation/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ unstable_navigationEvents.enable();
);
});
});
unstable_navigationEvents.saveCurrentPathname();

export default function Layout() {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ for more details.
</p>
</div>
<div
class="[&>*]:last:mb-0!"
class="border-palette-gray4 mb-5 overflow-hidden rounded-lg border shadow-xs [&_h2]:mt-0 [&_h3]:mt-0 [&_h4]:mt-0 [&_h3]:mb-1.5 [&_h4]:mb-0 [&_li]:mb-0 [&_thead]:border-palette-gray4 [&_th]:text-tertiary [&_th]:px-4 [&_td]:border-palette-gray4 [&_td]:py-3 [&_.table-wrapper]:border-palette-gray4 [&_.table-wrapper]:mb-0 [&_.table-wrapper]:shadow-none [&>*:last-child]:mb-0!"
>
<div
class="border-palette-gray4 border-t first:border-t-0"
Expand Down
6 changes: 4 additions & 2 deletions docs/components/plugins/api/APISectionProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
resolveTypeName,
} from './APISectionUtils';
import { APICommentTextBlock } from './components/APICommentTextBlock';
import { ELEMENT_SPACING, STYLES_SECONDARY, VERTICAL_SPACING } from './styles';
import { ELEMENT_SPACING, STYLES_APIBOX, STYLES_SECONDARY, VERTICAL_SPACING } from './styles';

export type APISectionPropsProps = {
data: PropsDefinitionData[];
Expand Down Expand Up @@ -115,7 +115,9 @@ const renderProps = (
.filter((dec, i, arr) => arr.findIndex(t => t?.name === dec?.name) === i);

return (
<div key={`props-definition-${def.name}`} className="[&>*]:last:mb-0!">
<div
key={`props-definition-${def.name}`}
className={mergeClasses(STYLES_APIBOX, '[&>*:last-child]:mb-0!')}>
{propsDeclarations?.map(prop =>
prop
? renderProp(
Expand Down
38 changes: 28 additions & 10 deletions docs/pages/build-reference/infrastructure.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
modificationDate: September 17th, 2025
modificationDate: May 12th, 2026
title: Build server infrastructure
sidebar_title: Server infrastructure
maxHeadingDepth: 4
Expand Down Expand Up @@ -44,15 +44,7 @@ Android builders run on virtual machines in an isolated environment. Every build

- [npm cache deployed with Kubernetes](/build-reference/caching/#javascript-dependencies)
- [Maven cache deployed with Kubernetes](/build-reference/caching/#android-dependencies)
- Global Gradle configuration in **~/.gradle/gradle.properties**:

```ini ~/.gradle/gradle.properties
org.gradle.jvmargs=-Xmx14g -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.daemon=false
```

- Gradle JVM args are injected via the `GRADLE_OPTS` environment variable when the build environment is provisioned. See [Gradle JVM args](#gradle-jvm-args) below.
- Global npm configuration in **~/.npmrc**:

```ini ~/.npmrc
Expand All @@ -68,6 +60,32 @@ Android builders run on virtual machines in an isolated environment. Every build
enableImmutableInstalls: false
```

### Gradle JVM args

EAS Build sets the `GRADLE_OPTS` environment variable on the build VM (the worker) before Gradle runs. The values depend on the [resource class](/eas/json/#resourceclass) you select:

| Resource class | `-Xmx` (max heap) |
| -------------- | ----------------- |
| `medium` | `4g` |
| `large` | `8g` |

In addition to `-Xmx`, the worker passes the following JVM args to the Gradle build JVM via `-Dorg.gradle.jvmargs`:

- `-XX:MaxMetaspaceSize=1g`
- `-XX:+HeapDumpOnOutOfMemoryError`
- `-Dfile.encoding=UTF-8`

The worker also sets these top-level Gradle properties on `GRADLE_OPTS`:

- `-Dorg.gradle.parallel=true`
- `-Dorg.gradle.daemon=false`

> **warning** The worker sets `org.gradle.jvmargs` via `GRADLE_OPTS`, which overrides any `org.gradle.jvmargs` defined in your project's **gradle.properties**.

#### Overriding `GRADLE_OPTS`

You can replace the worker default by setting `GRADLE_OPTS` under a build profile's [`env`](/eas/json/#env) in **eas.json**, in a [workflow file](/eas/workflows/syntax/#jobsjob_idenv), or with [EAS Environment Variables](/eas/environment-variables/). Project environment values take precedence over the worker's default values.

### Android server images

#### <CopyTextButton>`ubuntu-24.04-jdk-17-ndk-r27b-sdk-55` (`latest`, `sdk-55`)</CopyTextButton>
Expand Down
4 changes: 1 addition & 3 deletions docs/pages/versions/unversioned/sdk/ui/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,4 @@ Components are available for the following platforms:

## Drop-in replacements

API-compatible replacements for popular React Native community libraries:

- **[DateTimePicker](drop-in-replacements/datetimepicker)**: Compatible with `@react-native-community/datetimepicker`
See **[Drop-in replacements](drop-in-replacements)** for API-compatible replacements for popular React Native community libraries.
4 changes: 1 addition & 3 deletions docs/pages/versions/v55.0.0/sdk/ui/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,4 @@ Components are available for the following platforms:

## Drop-in replacements

API-compatible replacements for popular React Native community libraries:

- **[DateTimePicker](drop-in-replacements/datetimepicker)**: Compatible with `@react-native-community/datetimepicker`
See **[Drop-in replacements](drop-in-replacements)** for API-compatible replacements for popular React Native community libraries.
4 changes: 1 addition & 3 deletions docs/pages/versions/v56.0.0/sdk/ui/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,4 @@ Components are available for the following platforms:

## Drop-in replacements

API-compatible replacements for popular React Native community libraries:

- **[DateTimePicker](drop-in-replacements/datetimepicker)**: Compatible with `@react-native-community/datetimepicker`
See **[Drop-in replacements](drop-in-replacements)** for API-compatible replacements for popular React Native community libraries.
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import android.content.Context
import expo.modules.appmetrics.appstartup.AppStartupManager
import expo.modules.appmetrics.logevents.LogEventOptions
import expo.modules.appmetrics.logevents.Severity
import expo.modules.appmetrics.logevents.attributesToJsonObject
import expo.modules.appmetrics.logevents.sanitizeLogEventAttributes
import expo.modules.appmetrics.logevents.validateEventBody
import expo.modules.appmetrics.logevents.validateEventName
import expo.modules.appmetrics.memory.MemoryMetricsManager
import expo.modules.appmetrics.storage.JsMetric
import expo.modules.appmetrics.storage.JsSession
import expo.modules.appmetrics.storage.LogRecord
import expo.modules.appmetrics.storage.Metric
import expo.modules.appmetrics.storage.SessionManager
import expo.modules.appmetrics.updates.UpdatesMonitoring
import expo.modules.appmetrics.updates.UpdatesStateEvent
import expo.modules.appmetrics.utils.JsonAny
import expo.modules.appmetrics.utils.TimeUtils
import expo.modules.interfaces.constants.ConstantsInterface
import expo.modules.kotlin.exception.Exceptions
Expand All @@ -30,7 +31,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json

class AppMetricsModule : Module(), UpdatesStateChangeListener {
private val context: Context
Expand Down Expand Up @@ -97,7 +97,7 @@ class AppMetricsModule : Module(), UpdatesStateChangeListener {
name = validatedName,
body = validatedBody,
severity = severity.rawValue,
attributes = sanitized.attributes?.let { Json.encodeToString(attributesToJsonObject(it)) },
attributes = sanitized.attributes?.let { JsonAny.encodeMapToJsonString(it) },
droppedAttributesCount = sanitized.droppedCount
)
),
Expand Down Expand Up @@ -170,30 +170,8 @@ class AppMetricsModule : Module(), UpdatesStateChangeListener {

AsyncFunction("clearStoredEntries") Coroutine { -> sessionManager.clearAllData() }

Function("startSession") {
val sessionId = sessionManager.createSessionId()
val timestamp = TimeUtils.getCurrentTimestampInISOFormat()
val sessionMetadata = metadata

scope.launch {
sessionManager.startSessionWithIdAt(
sessionId = sessionId,
timestamp = timestamp,
metadata = sessionMetadata
)
}

return@Function sessionId
}

Function("stopSession") { sessionId: String ->
scope.launch {
sessionManager.stopSession(sessionId = sessionId)
}
}

AsyncFunction("addCustomMetricToSession") Coroutine { sessionId: String, metric: PartialMetric ->
sessionManager.addMetrics(listOf(metric.toMetric(sessionId)), sessionId = sessionId)
AsyncFunction("addCustomMetricToSession") Coroutine { metric: JsMetric ->
sessionManager.addMetrics(listOf(metric.toMetric()), sessionId = metric.sessionId)
}

AsyncFunction("getMainSession") Coroutine { ->
Expand Down Expand Up @@ -237,25 +215,6 @@ class AppMetricsModule : Module(), UpdatesStateChangeListener {
}
}

data class PartialMetric(
@Field val category: String,
@Field val name: String,
@Field val value: Double,
@Field val routeName: String? = null,
@Field val params: Map<String, Any>? = null
) : Record {
fun toMetric(sessionId: String): Metric =
Metric(
sessionId = sessionId,
timestamp = TimeUtils.getCurrentTimestampInISOFormat(),
category = category,
name = name,
value = value,
routeName = routeName,
params = params?.let { Json.encodeToString(it) }
)
}

data class MetricAttributes(
@Field val routeName: String? = null,
@Field val params: Map<String, Any>? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ enum class MetricCategory(
AppStartup("appStartup"),
FrameRate("frameRate"),
Memory("memory"),
Updates("updates")
Updates("updates"),
Navigation("navigation")
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package expo.modules.appmetrics.logevents

import android.util.Log
import expo.modules.appmetrics.TAG
import expo.modules.appmetrics.utils.JsonAny
import kotlinx.serialization.json.JsonObject

/**
* Patterns that match attribute keys reserved by the SDK. Caller-provided
Expand Down Expand Up @@ -115,13 +113,3 @@ internal fun sanitizeLogEventAttributes(attributes: Map<String, Any?>?): Sanitiz
droppedCount = emptyKeyDrops + reservedKeyDrops.size + overflowDrops
)
}

/**
* Converts a sanitized attribute map to a `JsonObject` for storage. Values
* whose type cannot be represented in OTLP (e.g. `Date`, NaN/Infinity doubles)
* are encoded as JSON `null` here; the typed-attribute encoder at dispatch
* time will skip them and add to `droppedAttributesCount` accordingly.
*/
internal fun attributesToJsonObject(attributes: Map<String, Any?>): JsonObject {
return JsonObject(attributes.mapValues { (_, value) -> JsonAny.toElement(value) })
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package expo.modules.appmetrics.storage

import expo.modules.appmetrics.utils.JsonAny
import expo.modules.appmetrics.utils.TimeUtils
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
import java.util.UUID
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
Expand Down Expand Up @@ -39,16 +41,29 @@ data class JsSession(
}

data class JsMetric(
@Field val metricId: String,
@Field val sessionId: String,
@Field val timestamp: String,
@Field val category: String,
@Field val name: String,
@Field val value: Double,
@Field val routeName: String?,
@Field val updateId: String?,
@Field val params: Map<String, Any?>?
@Field val metricId: String? = UUID.randomUUID().toString(),
@Field val timestamp: String = TimeUtils.getCurrentTimestampInISOFormat(),
@Field val routeName: String? = null,
@Field val updateId: String? = null,
@Field val params: Map<String, Any?>? = null
) : Record {
fun toMetric(): Metric =
Metric(
metricId = metricId ?: UUID.randomUUID().toString(),
sessionId = sessionId,
timestamp = timestamp,
category = category,
name = name,
value = value,
routeName = routeName,
updateId = updateId,
params = params?.let { JsonAny.encodeMapToJsonString(it) }
)

companion object {
fun fromMetric(metric: Metric): JsMetric =
JsMetric(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package expo.modules.appmetrics.utils

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
Expand Down Expand Up @@ -56,4 +57,13 @@ object JsonAny {
is JsonArray -> element.map { fromElement(it) }
}
}

// `Json.encodeToString(map)` would fail at runtime with "Serializer for class
// 'Any' is not found" — kotlinx.serialization has no built-in `Any`
// serializer, so we route every value through `toElement` first.
fun encodeMapToJsonString(map: Map<String, Any?>): String =
Json.encodeToString(
JsonObject.serializer(),
JsonObject(map.mapValues { (_, v) -> toElement(v) })
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package expo.modules.appmetrics.utils

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [28])
class JsonAnyTest {
@Test
fun `encodeMapToJsonString round-trips primitives, booleans and nested structures`() {
val encoded = JsonAny.encodeMapToJsonString(
mapOf(
"isInitial" to true,
"isAppLaunch" to false,
"screen" to "Home",
"attempt" to 3,
"ratio" to 1.5,
"missing" to null,
"tags" to listOf("a", "b"),
"nested" to mapOf("k" to true)
)
)

@Suppress("UNCHECKED_CAST")
val decoded = JsonAny.fromElement(Json.decodeFromString<JsonElement>(encoded)) as Map<String, Any?>
assertEquals(true, decoded["isInitial"])
assertEquals(false, decoded["isAppLaunch"])
assertEquals("Home", decoded["screen"])
assertEquals(3L, decoded["attempt"])
assertEquals(1.5, decoded["ratio"])
assertNull(decoded["missing"])
assertEquals(listOf("a", "b"), decoded["tags"])
assertEquals(mapOf("k" to true), decoded["nested"])
}
}
10 changes: 2 additions & 8 deletions packages/expo-app-metrics/build/module.web.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/expo-app-metrics/build/module.web.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading