Análitica y privacidad
Segment y Amplitude con puertas de consentimiento y alineación ATT en Expo SDK 57. La analítica de producto pertenece al programa de observabilidad, pero sigue reglas de privacidad que los crash reporters no.
Busca en todas las páginas de la documentación
Segment y Amplitude con puertas de consentimiento y alineación ATT en Expo SDK 57. La analítica de producto pertenece al programa de observabilidad, pero sigue reglas de privacidad que los crash reporters no.
npm install @segment/analytics-react-native @segment/sovran-react-native
# o Amplitude directamente:
npm install @amplitude/analytics-react-native
npx expo install expo-tracking-transparencyHerramientas: Estos ejemplos están dirigidos a Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 y React 19.2.3.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
1. Estado de consentimiento
// src/privacy/consent.ts
import AsyncStorage from "@react-native-async-storage/async-storage";
export type ConsentState = {
analytics: boolean;
crashReporting: boolean; // a menudo separado - puede estar activado por defecto para estabilidad
marketing: boolean;
decidedAt: string | null;
};
const KEY = "consent_v1";
const defaultState: ConsentState = {
analytics: false,
crashReporting: true,
marketing: false,
decidedAt: null,
};
export async function getConsent(): Promise<ConsentState> {
const raw = await AsyncStorage.getItem(KEY);
return raw ? JSON.parse(raw) : defaultState;
}
export async function setConsent(patch: Partial<ConsentState>) {
const next = { ...(await getConsent()), ...patch, decidedAt: new Date().toISOString() };
await AsyncStorage.setItem(KEY, JSON.stringify(next));
return next;
}2. Solicitud ATT (iOS)
// src/privacy/att.ts
import { Platform } from "react-native";
import * as TrackingTransparency from "expo-tracking-transparency";
export async function requestAttIfNeeded() {
if (Platform.OS !== "ios") return "unavailable";
const { status: existing } = await TrackingTransparency.getTrackingPermissionsAsync();
if (existing === "granted" || existing === "denied") return existing;
const { status } = await TrackingTransparency.requestTrackingPermissionsAsync();
return status;
}3. Fachada de analítica controlada
// src/analytics/index.ts
import { getConsent } from "@/privacy/consent";
type Props = Record<string, string | number | boolean | null>;
let segmentClient: { track: (e: string, p?: Props) => void; identify: (id: string, t?: Props) => void; reset: () => void } | null = null;
export async function initAnalytics() {
const consent = await getConsent();
if (!consent.analytics) return;
const { createClient } = await import("@segment/analytics-react-native");
segmentClient = createClient({
writeKey: process.env.EXPO_PUBLIC_SEGMENT_WRITE_KEY!,
trackAppLifecycleEvents: true,
});
}
export async function track(event: string, properties?: Props) {
const consent = await getConsent();
if (!consent.analytics || !segmentClient) return;
segmentClient.track(event, sanitize(properties));
}
export async function identify(userId: string, traits?: Props) {
const consent = await getConsent();
if (!consent.analytics || !segmentClient) return;
segmentClient.identify(userId, sanitize(traits));
}
export async function resetAnalytics() {
segmentClient?.reset();
}
function sanitize(props?: Props): Props | undefined {
if (!props) return props;
const blocked = ["email", "phone", "name", "address"];
return Object.fromEntries(
Object.entries(props).filter(([k]) => !blocked.includes(k.toLowerCase()))
);
}4. Puerta de consentimiento en el diseño raíz
// src/privacy/ConsentGate.tsx
import { useEffect, useState } from "react";
import { Button, Text, View } from "react-native";
import { getConsent, setConsent } from "./consent";
import { requestAttIfNeeded } from "./att";
import { initAnalytics } from "@/analytics";
export function ConsentGate({ children }: { children: React.ReactNode }) {
const [ready, setReady] = useState(false);
const [showBanner, setShowBanner] = useState(false);
useEffect(() => {
(async () => {
const consent = await getConsent();
setShowBanner(!consent.decidedAt);
if (consent.analytics) await initAnalytics();
setReady(true);
})();
}, []);
if (!ready) return null;
if (showBanner) {
return (
<View style={{ padding: 24, gap: 12 }}>
<Text>Usamos analítica para mejorar la aplicación. Puedes cambiar esto en Configuración.</Text>
<Button
title="Aceptar analítica"
onPress={async () => {
await requestAttIfNeeded();
await setConsent({ analytics: true });
await initAnalytics();
setShowBanner(false);
}}
/>
<Button
title="Rechazar"
onPress={async () => {
await setConsent({ analytics: false });
setShowBanner(false);
}}
/>
</View>
);
}
return <>{children}</>;
}Cuándo usar esto:
Segment - Amplitude con fuentes específicas del entorno y reinicio de logout.
// app.config.ts - declara NSUserTrackingUsageDescription para iOS
export default {
expo: {
ios: {
infoPlist: {
NSUserTrackingUsageDescription:
"This identifier helps us measure app performance and improve features.",
},
},
},
};// app/_layout.tsx
import { Stack } from "expo-router";
import { ConsentGate } from "@/privacy/ConsentGate";
export default function RootLayout() {
return (
<ConsentGate>
<Stack />
</ConsentGate>
);
}// src/features/auth/logout.ts
import { resetAnalytics } from "@/analytics";
import * as SecureStore from "expo-secure-store";
export async function logout() {
await SecureStore.deleteItemAsync("refresh_token");
await resetAnalytics();
// limpiar estado React, invalidar consultas...
}// Esquema de eventos estable - versionea las propiedades con cuidado
await track("checkout_completed", {
order_id: order.id,
item_count: order.items.length,
currency: "USD",
value_cents: order.totalCents,
});| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Router de Segment | Un SDK, muchos destinos, filtrado del lado del servidor | Costo extra, otro proveedor |
| Amplitude directo | UI de analítica de producto enriquecida, experimentación | Cada herramienta nueva necesita otro SDK |
| Ambos | Segment - Amplitude común en empresas | Dos configuraciones para mantener sincronizadas |
// Amplitude directo (cuando Segment no es necesario)
import { init, track, identify, reset } from "@amplitude/analytics-react-native";
export async function initAmplitude() {
await init(process.env.EXPO_PUBLIC_AMPLITUDE_API_KEY!, undefined, {
trackingOptions: { ipAddress: false },
});
}account_id de B2B - no para PIIrelease y otaUpdateId como super-propiedades para correlación de lanzamiento - Aspectos básicos de actualizaciones OTARelacionado: Aspectos básicos de observabilidad - no confundas
track()concaptureException()
checkout_completed, no completeCheckoutschema_version si es necesariouser_id opaco de autenticación, nunca correo electrónicoscreen_view tanto del router como del useEffect de pantallacold_start_ms) pertenecen aquí o en Monitoreo de rendimiento - elige un propietariodecidedAt se establezca.Versiones de la pila: Esta página fue escrita para React 19.2.3, React Native 0.86.0 y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026