Когда optional SDK ведут себя как core infrastructure

Типичный launch failure в vibe-coded mobile-приложениях выглядит так: analytics, attribution или crash reporting инициализируются вместе с реально критичными сервисами. Если один из этих optional SDK начинает вести себя плохо на конкретном устройстве или в конкретном сетевом условии, всё приложение может умереть ещё до первого экрана.

Такое происходит потому, что AI-generated setup code склонен считать каждую dependency одинаково важной. Без явной tier-системы исчезает архитектурное различие между must-have infrastructure и nice-to-have observer’ами. В итоге всё случайно становится must-have.

Три уровня зависимостей

Любая зависимость в приложении попадает в один из трёх бакетов. Если вы явно проводите эти границы, приложение переживает рост. Если нет - умирает от тысячи продовых крэшей:

  • Hard dependencies: если они не работают, приложение не может отрисоваться. Парсинг конфигурации. Создание storage. Конструирование core services. Это ваш фундамент.
  • Soft dependencies: могут деградировать без падения. Auth. Фичи, завязанные на сеть. Флоу, завязанные на permissions. Shell приложения продолжает рендериться; пользователь видит урезанный опыт, а не crash.
  • Optional dependencies: могут падать молча. Аналитика. Crash reporting. A/B testing. Они никогда не блокируют приложение. Это наблюдатели, а не участники.

Главная ошибка большинства vibe-coded приложений в том, что они считают hard буквально всё. Каждый SDK инициализируется на старте. Любая ошибка становится startup crash. Но ваш analytics SDK не заслуживает того же статуса, что слой storage.

Граница hard dependencies

Hard boundary валидирует конфиг и собирает сервисы. Если что-то ломается, приложение показывает экран ошибки с кнопкой retry, а не белую пустоту, за которую пользователи поставят одну звезду в App Store.

// src/context/hard-dependency-boundary.tsx
// This is the gatekeeper. If it fails, nothing else renders.

import * as SplashScreen from 'expo-splash-screen'

void SplashScreen.preventAutoHideAsync()

export function HardDependencyBoundary({
  children,
}: {
  children: (services: Services) => ReactNode
}) {
  const { services, ready, error, retry } =
    useHardDependencies()

  useEffect(() => {
    if (ready || error) void SplashScreen.hideAsync()
  }, [ready, error])

  if (error) {
    return (
      <ErrorScreen
        title='Startup failed'
        message={error.message}
        onRetry={retry}
      />
    )
  }

  if (!ready || !services) return null

  return <>{children(services)}</>
}

Обратите внимание на render prop pattern. Boundary передаёт инициализированные сервисы детям. Дочерние компоненты не создают сервисы, а получают их снаружи. Конструирование происходит ровно один раз, и любая ошибка ловится на boundary, а не утекáет в production crash report.

Optional boundary

Теперь сравните это с optional dependencies. Они никогда не должны ронять приложение. Если Mixpanel лежит, приложение не должно даже вздрогнуть:

// src/context/optional-dependency-boundary.tsx
// If analytics fails, we log a warning and keep going.

export function OptionalDependencyBoundary({
  children,
}: PropsWithChildren) {
  const { analytics, crashReporting } = useServices()

  useEffect(() => {
    try {
      analytics.track('app_started')
    } catch (error) {
      if (__DEV__) console.warn('Analytics failed:', error)
    }

    try {
      crashReporting.setContext('app', { started: true })
    } catch (error) {
      if (__DEV__) console.warn('Crash reporting failed:', error)
    }
  }, [analytics, crashReporting])

  return <>{children}</>
}

Разница в подходе очень заметна. Hard boundary: если ломается, показываем error screen. Optional boundary: если ломается, пишем warning в лог и идём дальше. Оба сценария описаны явно. Ни один не притворяется другим.

Большинство production crash в vibe-coded приложениях возникают из-за одной ошибки: soft или optional dependency трактуют как hard dependency. Autotomy Expo Starter Pack проводит эти границы с первого дня, чтобы вы не усвоили этот урок в 2 часа ночи.