npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cariva-dev/exercise-sdk

v0.0.8-alpha-1

Published

Cariva exercise SDK

Readme

cariva-exercise-sdk

React Native SDK for reading exercise and activity data from Android Health Connect.

Published package: @cariva-dev/exercise-sdk

Platform Support

| Platform | Status | |----------|--------| | Android | Supported (Health Connect) | | iOS | Out of scope for this release |


Requirements

  • React Native 0.68+ (New Architecture / Turbo Module compatible)
  • Android app minSdkVersion 24 or higher
  • A device or emulator with Health Connect support
  • Health Connect installed on Android 13 and lower, or available through Play Services / system components on Android 14+

Integration Guide

For consumer setup in an existing React Native Android app, start with docs/integration/react-native-android-setup.md.

The guide covers:

  • package installation with @cariva-dev/exercise-sdk
  • Android manifest merge expectations
  • integration verification checklist
  • minimal implementation example
  • common troubleshooting paths

Architecture

The SDK now follows a Clean/Hexagonal structure:

  • android/.../domain: core models and algorithm services
  • android/.../application: use-cases and ports
  • android/.../infrastructure: Health Connect/Android adapters
  • android/.../HealthConnectModule.kt: thin React Native bridge/controller
  • src/domain, src/application, src/infrastructure: mirrored TypeScript boundary layers

See docs/architecture/clean-hexagonal-migration.md for migration details.


Installation

1. Install the package

# npm
npm install @cariva-dev/exercise-sdk

# yarn
yarn add @cariva-dev/exercise-sdk

2. Android setup

The SDK ships its own Health Connect permissions, package visibility queries, and permission rationale activities in the library manifest.

In a standard React Native Android app, these entries are merged automatically into your app during the Android build.

Follow the integration guide for the verification checklist and fallback steps if a custom Android setup prevents normal manifest merge behavior.

3. iOS

No additional setup is required because iOS is not a supported runtime path in this release. The repository currently ships only a minimal iOS stub, and the JavaScript adapter should still be treated as Android-only unless explicit iOS support is added.


Quick Start

import {
  connect,
  disconnect,
  getExerciseData,
  setAllowDatasource,
  getAllowDatasource,
  resetPermission,
} from '@cariva-dev/exercise-sdk';

// 1. Connect and request permissions
const status = await connect({
  onMissingApp: 'status',
  requestPermission: true,
  permissionScope: ['steps', 'calories'],
});

if (!status.ready) {
  console.log('Next action required:', status.nextAction);
  // e.g. 'INSTALL_HEALTH_CONNECT' | 'REQUEST_PERMISSION' | ...
  return;
}

// 2. Fetch exercise data
const data = await getExerciseData({
  sinceMillis: Date.now() - 7 * 24 * 60 * 60 * 1000, // last 7 days
  untilMillis: Date.now(),
  type: 'all',
  bucketPeriod: 'daily',
});

console.log('Records:', data.records);
console.log('Active kcal:', data.summary.activeKcal);
console.log('Time zone:', data.meta.timeZone);

// 3. Disconnect and clear granted Health Connect permissions
await disconnect();

// 4. Revoke then re-request permissions
await resetPermission({ permissionScope: ['steps', 'calories'] });

API Reference

connect(options?)

Checks Health Connect availability and optionally requests permissions.

type ConnectOptions = {
  onMissingApp?: 'status' | 'open';   // 'open' navigates to Play Store
  requestPermission?: boolean;          // default: true
  permissionScope?: Array<'steps' | 'activetimes' | 'calories' | 'distances'>;
};

type ConnectResult = {
  ready: boolean;
  healthConnectInstalled: boolean;
  healthConnectStatus: 'AVAILABLE' | 'PROVIDER_UPDATE_REQUIRED' | 'UNAVAILABLE' | 'UNKNOWN';
  hasPermissions: boolean;
  nextAction:
    | 'INSTALL_HEALTH_CONNECT'
    | 'UPDATE_HEALTH_CONNECT'
    | 'REQUEST_PERMISSION'
    | 'NONE';
  requestedPermissions?: string[];
};

| Option | Type | Default | Description | |--------|------|---------|-------------| | onMissingApp | 'status' \| 'open' | 'status' | 'open' opens Play Store for the missing app | | requestPermission | boolean | true | Trigger the Health Connect permission dialog | | permissionScope | PermissionScope[] | all scopes | Limit which permissions are requested |

activetimes may request both exercise-session and active-calories read permissions so the native fallback reader can still derive active duration when session records are unavailable.


disconnect()

Revokes granted Health Connect permissions and clears the SDK's in-memory datasource blacklist.

const result = await disconnect();
// => { success: true }

resetPermission(options?)

Revokes existing Health Connect permissions and immediately runs the normal permission request flow again.

type ResetPermissionOptions = {
  onMissingApp?: 'status' | 'open';
  permissionScope?: Array<'steps' | 'activetimes' | 'calories' | 'distances'>;
};

const status = await resetPermission({
  permissionScope: ['steps', 'calories'],
});

resetPermission returns the same ConnectResult shape as connect.


getExerciseData(options)

Reads aggregated exercise data from Health Connect.

type GetExerciseDataOptions = {
  sinceMillis: number;
  untilMillis: number;
  type?: 'steps' | 'activetimes' | 'calories' | 'distances' | 'all'; // default: 'all'
  bucketPeriod?: 'hourly' | 'daily';                                    // default: 'hourly'
  missingBasalPolicy?: 'strict' | 'latest_or_zero';                     // default: 'strict'
  debug?: boolean;                                                       // default: false
  includeEmptyBuckets?: boolean;                                         // default: true
};

type UnifiedIntervalRecord = {
  steps: number;
  activeCalories: number;
  activeCaloriesUnit: 'kcal';
  activeCaloriesSource: 'device' | 'app' | 'manual_input' | 'unknown' | 'fallback';
  basalCalories: number;
  basalCaloriesUnit: 'kcal';
  distance: number;
  distanceUnit: 'km';
  activeTime: number;
  activeTimeUnit: 'milliseconds';
  startDate: string;
  endDate: string;
};

type ExerciseData = {
  summary: {
    steps: number;
    activeKcal: number;
    distanceMeters: number;
    activeTimeMillis: number;
  };
  meta: {
    timeZone: string;
    bucketPeriod: 'hourly' | 'daily';
    units: {
      calories: 'kcal';
      distance: 'km';
      activeTime: 'milliseconds';
    };
  };
  notices?: Array<{
    code: string;
    severity: 'info' | 'warning';
  }>; // debug only
  debug?: {
    integrity: {
      projectionPreserved: boolean;
      roundingScale: number;
    };
    warnings: Array<{
      code: string;
      severity: 'info' | 'warning' | 'critical';
      message?: string;
      context?: Record<string, string | number | boolean | null>;
    }>;
    diagnostics?: {
      fallbackUsedSegments: number;
      fallbackSuppressedSegments: number;
      zeroBasalFallbackUsedSegments: number;
      idleGateSuppressedSegments: number;
      basalSeedApplied: boolean;
      seededFromBeforeSince: boolean;
      basalSeedAgeHours?: number;
      directActiveCaloriesKcal: number;
      totalCaloriesKcal: number;
      basalCaloriesKcal: number;
      mergedActiveCaloriesKcal: number;
      fallbackContributionKcal: number;
    };
  };
  records: UnifiedIntervalRecord[];
  totalSteps?: number; // deprecated, debug only
  totalKcal?: number; // deprecated, equivalent to summary.activeKcal, debug only
  totalBasalKcal?: number; // deprecated, debug only
  totalDistanceMeters?: number; // deprecated, debug only
  totalActiveTimeMillis?: number; // deprecated, debug only
  timeZone?: string; // deprecated, use meta.timeZone, debug only
  warnings?: string[]; // deprecated legacy warning codes, debug only
  integrity?: {
    projectionPreserved: boolean;
    roundingScale: number;
  }; // deprecated, populated only when debug is enabled
  diagnostics?: {
    fallbackUsedSegments: number;
    fallbackSuppressedSegments: number;
    zeroBasalFallbackUsedSegments: number;
    idleGateSuppressedSegments: number;
    basalSeedApplied: boolean;
    seededFromBeforeSince: boolean;
    basalSeedAgeHours?: number;
    directActiveCaloriesKcal: number;
    totalCaloriesKcal: number;
    basalCaloriesKcal: number;
    mergedActiveCaloriesKcal: number;
    fallbackContributionKcal: number;
  }; // deprecated compatibility diagnostics, debug only
};

Periods longer than 90 days reject with error code E_INVALID_PERIOD.

Health Connect also applies runtime read quotas, with stricter limits for background work. The SDK keeps a short-lived exact-query cache for successful getExerciseData() responses, so repeated identical refreshes within 15 seconds can reuse the last result instead of re-reading Health Connect.

To further reduce quota pressure, the Android reader now paces internal Health Connect sub-reads, including paginated reads and retry attempts, and performs one fixed 5-second cooldown retry when Health Connect reports a rate-limit error. If the retry still fails, the SDK surfaces the existing E_GET_EXERCISE_DATA error path.

To maximize cache hits and avoid unnecessary quota use, keep sinceMillis and untilMillis stable within a refresh cycle instead of shifting the window on every poll, and let your scheduler or caller back off repeated background sync attempts. A full changelog-based sync strategy is out of scope for this SDK patch.

Preferred response contract:

  • summary.activeKcal is the active-calorie total and matches the legacy totalKcal
  • records is the bucket-by-bucket array returned for the requested bucketPeriod
  • includeEmptyBuckets: false filters only the projected records; it does not affect cached/internal responses, summary, or debug-only deprecated totals
  • debug: true adds debug.integrity, structured debug.warnings, and debug.diagnostics when real diagnostics exist
  • debug: true also exposes legacy top-level totals, warnings, integrity, and diagnostics for compatibility with older troubleshooting flows
  • absence of debug.diagnostics remains meaningful; the SDK does not synthesize zero-valued diagnostics objects

Why this split exists:

  • Normal app flows usually need totals plus per-bucket activity metrics, not internal projection or audit details.
  • summary and meta give new consumers a cleaner default surface for totals and shared units without removing the existing per-bucket schema.
  • debug keeps troubleshooting and audit data opt-in for QA, investigations, and internal support work.
  • records stays as the single per-bucket surface in this release so existing consumers do not need to switch arrays.

Recommended field usage:

  • Use summary for totals across the full query window.
  • Use records for charts, timelines, and bucket-by-bucket UI.
  • Use meta for shared response metadata such as timeZone, bucketPeriod, and units.
  • Use notices only in debug flows when you want lightweight warning summaries alongside the full debug payload.
  • Use debug only when investigating data quality, fallback behavior, or projection issues.
  • Use deprecated top-level totals only for backward compatibility in debug flows.

setAllowDatasource(config)

Configures which data sources to exclude from results.

type DatasourceType = 'manual_input' | 'device' | 'app' | 'unknown';

type DatasourceConfig = {
  blacklist: DatasourceType[];
};

// Example: exclude manually-entered data
await setAllowDatasource({ blacklist: ['manual_input'] });

getAllowDatasource()

Returns the current datasource blacklist configuration.

const config = await getAllowDatasource();
// => { blacklist: ['manual_input'] }

Bucket Record Schema

Use data.records for per-bucket results:

type UnifiedIntervalRecord = {
  steps: number;
  activeCalories: number;
  activeCaloriesUnit: 'kcal';
  activeCaloriesSource: 'device' | 'app' | 'manual_input' | 'unknown' | 'fallback';
  basalCalories: number;
  basalCaloriesUnit: 'kcal';
  distance: number;
  distanceUnit: 'km';
  activeTime: number;
  activeTimeUnit: 'milliseconds';
  startDate: string; // ISO offset string, e.g. "2026-02-16T08:00:00+07:00"
  endDate: string;
};

records are grouped by the requested bucketPeriod. The first and last record may be partial buckets when sinceMillis or untilMillis land mid-hour or mid-day.

data.meta.units still exposes the shared units once per response:

{
  calories: 'kcal',
  distance: 'km',
  activeTime: 'milliseconds'
}

The current record shape still includes per-record unit fields and basalCalories for compatibility with existing consumers.

When includeEmptyBuckets is false, the SDK filters records only at the response projection stage. Internal aggregation, cache entries, summary, and debug-only deprecated totals remain unchanged.

basalCalories reflects the bucketed basal energy for that same interval, and totalBasalKcal mirrors the summed basal total across the whole response window when debug: true exposes legacy totals.

Example record:

{
  "steps": 1300,
  "activeCalories": 60.2,
  "activeCaloriesUnit": "kcal",
  "activeCaloriesSource": "fallback",
  "basalCalories": 42.5,
  "basalCaloriesUnit": "kcal",
  "distance": 1.0,
  "distanceUnit": "km",
  "activeTime": 1200000,
  "activeTimeUnit": "milliseconds",
  "startDate": "2026-02-16T08:00:00+07:00",
  "endDate": "2026-02-16T09:00:00+07:00"
}

Overlap Handling

The SDK automatically de-duplicates overlapping Health Connect records:

| Metric | Strategy | |--------|----------| | steps | Source priority: device > app > unknown > manual_input | | activetimes, calories, distances | Proportional-by-time allocation |


Calories Fallback (Total - Basal)

When direct active calories are unavailable or lower quality in some windows, the SDK can derive fallback active calories:

active = max(totalCalories - basalCalories, 0)

Policy and guarantees:

  • Pre-since basal seed lookup uses the latest BasalMetabolicRateRecord before sinceMillis, with no age cap.
  • If basal coverage is missing for a window, fallback is strictly suppressed for that window.
  • If the window has no movement/direct evidence, fallback is suppressed (idle gate).
  • Direct active-calorie measurement is preferred when present; fallback fills only uncovered windows.
  • Merge remains deterministic and avoids double counting across atomic windows.
  • Warnings and diagnostics are emitted for auditability.

Typical warning keys:

  • calories_fallback_total_minus_basal_used
  • calories_fallback_only_used
  • calories_fallback_partial_used
  • calories_fallback_zero_basal_used
  • bmr_seed_from_latest_before_since_used
  • bmr_missing_for_fallback_window
  • fallback_suppressed_due_to_missing_basal
  • fallback_suppressed_due_to_idle_window
  • fallback_suppressed_due_to_direct_active
  • movement_evidence_partially_unavailable_for_fallback_gate

Notices, Integrity, and Debug

When debug: true is enabled, data.notices provides a lightweight warning surface:

type ExerciseNotice = {
  code: string;
  severity: 'info' | 'warning';
};

data.warnings, data.integrity, and data.diagnostics are legacy compatibility fields that appear only when debug: true is passed.

data.debug is opt-in and only appears when debug: true is passed:

{
  integrity: {
    projectionPreserved: boolean;
    roundingScale: number;
  };
  warnings: Array<{
    code: string;
    severity: 'info' | 'warning' | 'critical';
  }>;
  diagnostics?: {
    // Only included when real diagnostics exist
  };
}

React Native UI pattern (non-blocking):

const hasNotices = !!data.notices?.length;

return (
  <View>
    {hasNotices && <Text>Data notices available</Text>}
    <Text>{`Active kcal: ${data.summary.activeKcal}`}</Text>
    <Text>{`Projection preserved: ${data.debug?.integrity.projectionPreserved ?? false}`}</Text>
  </View>
);

Examples

Normal mode

import {
  connect,
  getExerciseData,
  setAllowDatasource,
} from '@cariva-dev/exercise-sdk';

async function loadHealthData() {
  // Exclude manual entries
  await setAllowDatasource({ blacklist: ['manual_input'] });

  // Connect with minimal permission scope
  const status = await connect({
    onMissingApp: 'status',
    requestPermission: true,
    permissionScope: ['steps', 'calories'],
  });

  if (!status.ready) {
    console.warn('Health Connect not ready. Next action:', status.nextAction);
    return null;
  }

  // Query last 24 hours in hourly buckets
  const data = await getExerciseData({
    sinceMillis: Date.now() - 24 * 60 * 60 * 1000,
    untilMillis: Date.now(),
    type: 'all',
    bucketPeriod: 'hourly',
  });

  console.log(data.summary.activeKcal);
  console.log(data.records);

  return data;
}

Debug mode

const data = await getExerciseData({
  sinceMillis: Date.now() - 24 * 60 * 60 * 1000,
  untilMillis: Date.now(),
  type: 'all',
  bucketPeriod: 'hourly',
  debug: true,
});

console.log(data.debug?.integrity);
console.log(data.debug?.warnings);
console.log(data.debug?.diagnostics);

Filter empty records

const data = await getExerciseData({
  sinceMillis: Date.now() - 24 * 60 * 60 * 1000,
  untilMillis: Date.now(),
  type: 'all',
  bucketPeriod: 'hourly',
  includeEmptyBuckets: false,
});

console.log(data.records); // filtered
console.log(data.summary); // totals remain unchanged

Migration (2.0.0)

This release introduces a clean-break connect contract:

  • Removed ConnectResult.googleFitInstalled
  • Removed nextAction: 'INSTALL_GOOGLE_FIT'
  • connect readiness flow is now Health Connect-only

If you previously handled Google Fit branches, delete those checks and rely on:

  • INSTALL_HEALTH_CONNECT
  • UPDATE_HEALTH_CONNECT
  • REQUEST_PERMISSION
  • NONE

Calories now may include optional diagnostics for fallback audit details (total - basal path). Unified records now include activeCaloriesSource so consumers can distinguish direct vs fallback contribution at bucket level.

Additive migration notes for getExerciseData

  • Prefer summary + meta + records for new integrations.
  • summary.activeKcal is the explicit active-calorie total and corresponds to the legacy totalKcal.
  • includeEmptyBuckets: false now filters the projected records view only; totals still come from the unfiltered aggregation result.
  • records continue to use the existing UnifiedIntervalRecord schema in this release.
  • notices, legacy top-level totals, warnings, integrity, and diagnostics now appear only when debug: true is enabled.