# Mobile apps and PWAs

What ProductLense tracks in native iOS and Android apps, progressive web apps and web views, and which guide to follow for each.

ProductLense tracks native apps with its SDKs and web pages with its script, and reports both side by side: the Hostname breakdown shows each app's bundle id next to your website.

| App | Install |
|---|---|
| React Native or Expo | [`@productlense/react-native`](/docs/install/react-native) |
| Flutter | [`productlense_analytics`](/docs/install/flutter) |
| Native iOS (Swift) | [`ProductLense`](/docs/install/ios) |
| Native Android (Kotlin) | [`productlense-android`](/docs/install/android) |
| Anything else | [Events API from your backend](#native-screens-from-your-backend) |
| Progressive web app | the [script tag](/docs/install/script-tag), as below |
| Pages in a web view | the script in the page, as below |

## Progressive web apps

An installed PWA loads the same pages, so the same tag works with no changes. Navigation inside the app is recorded like any single-page app.

- A PWA launched from the home screen usually has no referrer, so its visits count as `direct`. Add UTM tags to the manifest's start URL to tell them apart: `"start_url": "/?utm_source=pwa&utm_medium=app"`.
- Events made while the device is offline are not queued for later. They are lost, like any request that cannot be sent.
- If a service worker caches pages, make sure it does not cache `https://api.productlense.app/a.js` forever: let it follow the `Cache-Control` header, which is one day.

```json title="manifest.webmanifest"
{
  "name": "Example",
  "start_url": "/?utm_source=pwa&utm_medium=app",
  "display": "standalone"
}
```

## Web views in a native app

Pages opened in `WKWebView`, Android `WebView` or a React Native `WebView` run the script normally; the [React Native guide](/docs/install/react-native) shows how to pass the signed-in user into one. Two things differ:

- Each web view has its own storage, so an `identify` made in the browser does not carry into the app. Call `identify` inside the web view once the user is known.
- Some apps strip the referrer when they open a web view. Add UTM tags to the URLs your app opens.

## Native screens from your backend

Without an SDK (native Swift or Kotlin apps, for now), send events from your backend with the [Events API](/docs/api/events), passing the user id and the device's IP address and user agent as your API saw them. This needs an API key with the `events` scope, which stays on your server. Never put an API key in an app binary.

```bash
curl https://api.productlense.app/api/v1/events \
  -H "Authorization: Bearer $PL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"s":"SITE_ID","e":"Trial started","u":"https://example.com/app/trial","uid":"user_123","ip":"203.0.113.7","ua":"ExampleApp/2.4 (iPhone; iOS 19.0)"}'
```

> [!IMPORTANT]
> User agents that look like HTTP libraries are dropped as bots. `okhttp` (the default on Android) and `Electron` are counted only when the event carries a screen width (`w`); add `"w"` to native events, or have the app send a descriptive user agent such as `ExampleApp/2.4 (Android 16)` and forward that one. The full list is in [Bots and filtering](/docs/track/bots-and-filtering#app-clients).

The `u` field must be a URL on one of your site's hostnames. Use a path that names the screen, such as `/app/trial`. In-app purchases made through Apple or Google are read by the [RevenueCat connection](/docs/revenue/revenuecat); payments through your web checkout or the [Payment API](/docs/revenue/payments-api) are attributed too.

## Verify your installation

1. For a PWA or web view, open the app on a device and load a page.
2. Open the Live page in the ProductLense dashboard. The visit appears within a few seconds.
3. For backend events, the Events API answers `204`, and the event shows in the Live page and under Goals.

If nothing appears, follow [Verify your installation](/docs/install/verify).
