Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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: 1 addition & 0 deletions .github/actions/project-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ runs:
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive

- name: Setup Java
uses: actions/setup-java@v4
Expand Down
126 changes: 126 additions & 0 deletions .github/workflows/bump-kmp-submodule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Bump KMP Submodule

# Reusable workflow: re-points the OneSignal-KMP-SDK git submodule to a released tag
# and opens a PR. Called by the KMP repo's Release workflow (the fan-out pattern used
# by OneSignal/sdk-shared), or run manually via workflow_dispatch.
#
# Cross-repo callers authenticate by passing the shared org push token as
# GH_PUSH_TOKEN: a workflow triggered from the KMP repo runs with the KMP repo's
# GITHUB_TOKEN, which cannot write here, so GH_PUSH_TOKEN (used across the OneSignal
# SDK repos) is required for the checkout/push/PR to target this repo.

on:
workflow_call:
inputs:
kmp_version:
description: "OneSignal-KMP-SDK tag to pin the submodule to (e.g. v0.2.0)"
required: true
type: string
kmp_sha:
description: "Commit SHA the tag points at (the gitlink target)"
required: true
type: string
secrets:
GH_PUSH_TOKEN:
required: false
description: Cross-repo push token (contents + pull-requests write)
workflow_dispatch:
inputs:
kmp_version:
description: "OneSignal-KMP-SDK tag to pin the submodule to (e.g. v0.2.0)"
required: true
type: string
kmp_sha:
description: "Commit SHA the tag points at (the gitlink target)"
required: true
type: string

permissions:
contents: write
pull-requests: write

jobs:
bump:
runs-on: ubuntu-latest
env:
SUBMODULE_PATH: OneSignal-KMP-SDK
VERSION: ${{ inputs.kmp_version }}
NEW_SHA: ${{ inputs.kmp_sha }}
steps:
- name: "[Checkout] Android SDK"
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_PUSH_TOKEN || github.token }}
# Only the mode-160000 gitlink SHA is rewritten, so the submodule contents
# are not needed (no recursive checkout).

- name: "[Setup] Git user"
uses: OneSignal/sdk-shared/.github/actions/setup-git-user@main

- name: "[Bump] Re-point submodule gitlink"
id: bump
run: |
set -euo pipefail
branch="chore/bump-kmp-${VERSION}"
echo "branch=$branch" >> "$GITHUB_OUTPUT"

git checkout -b "$branch"
# Update the mode-160000 gitlink entry to the commit the tag points at.
git update-index --add --cacheinfo "160000,${NEW_SHA},${SUBMODULE_PATH}"

# No-op guard: if the pointer already matches (e.g. the very first release,
# where main already points at the tagged commit), skip the commit/PR
# instead of failing on an empty commit.
if git diff --cached --quiet; then
echo "Submodule already at ${NEW_SHA}; nothing to bump."
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "changed=true" >> "$GITHUB_OUTPUT"
git commit -m "chore(logger): bump ${SUBMODULE_PATH} submodule to ${VERSION}"
git push origin "$branch" --force-with-lease

- name: "[PR] Open or update bump PR"
if: ${{ steps.bump.outputs.changed == 'true' }}
env:
GH_TOKEN: ${{ secrets.GH_PUSH_TOKEN || github.token }}
run: |
set -euo pipefail
branch="${{ steps.bump.outputs.branch }}"
notes_url="https://github.com/OneSignal/OneSignal-KMP-SDK/releases/tag/${VERSION}"

# Best-effort: pull the published release notes for this tag so the reviewer can
# read exactly what the new version contains before merging the bump, without
# leaving the PR. Reading another repo's release needs cross-repo access, which
# GH_PUSH_TOKEN has; on any failure (missing token, release not yet visible) we
# fall back to just linking the release page.
notes="$(gh release view "$VERSION" --repo OneSignal/OneSignal-KMP-SDK --json body --jq .body 2>/dev/null || true)"

body_file="$RUNNER_TEMP/bump_pr_body.md"
{
printf 'Automated bump of the `%s` submodule to **%s** (`%s`).\n\n' "$SUBMODULE_PATH" "$VERSION" "$NEW_SHA"
printf '## Release notes — %s\n\n' "$VERSION"
if [ -n "$notes" ]; then
printf '%s\n\n' "$notes"
printf '_Source: [%s](%s)_\n\n' "$VERSION" "$notes_url"
else
printf 'See [%s](%s).\n\n' "$VERSION" "$notes_url"
fi
printf 'After merge, run `git submodule update --init --recursive` locally to sync.\n'
} > "$body_file"

# Reuse the PR if one is already open for this branch (re-runs refresh title/body).
if gh pr view "$branch" >/dev/null 2>&1; then
echo "PR already exists for $branch; refreshing title and release notes."
gh pr edit "$branch" \
--title "chore(logger): bump shared KMP logger to ${VERSION}" \
--body-file "$body_file"
else
gh pr create \
--base main \
--head "$branch" \
--title "chore(logger): bump shared KMP logger to ${VERSION}" \
--body-file "$body_file"
fi
2 changes: 2 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
submodules: recursive

# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

# Fail fast on unset var. Otherwise `demoOverride()` collapses "" to
# null and the build silently uses the hardcoded default app ID.
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ jobs:
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.ref || github.sha }}
submodules: recursive

# actions/checkout does not fetch tag refs into submodules (fetch-depth: 1).
# The shared logger stamps LoggerBuildInfo.KMP_VERSION via `git describe --tags`
# and refuses to publish without a vX.Y.* provenance — so tags must be present.
- name: Fetch OneSignal-KMP-SDK tags (logger provenance)
run: |
git -C OneSignal-KMP-SDK fetch --tags --force
echo "KMP provenance: $(git -C OneSignal-KMP-SDK describe --tags --always --dirty)"

- name: Detect current branch
id: detect_branch
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "OneSignal-KMP-SDK"]
path = OneSignal-KMP-SDK
url = git@github.com:OneSignal/OneSignal-KMP-SDK.git
59 changes: 59 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,65 @@ If your proposed contribution is a small bug fix, please feel free to create you

If your contribution would _break_ or _change_ the functionality of the SDK, please reach out to us on (contact) before you put in a lot of effort into a change we may not be able to use. We try our best to make sure that the SDK remains stable so that developers do not have to continually change their code, however some breaking changes _are_ desirable, so please get in touch to discuss your idea before you put in a lot of effort.

### Building the SDK from source

The shared Kotlin Multiplatform `:logger` module lives in the standalone [OneSignal-KMP-SDK](https://github.com/OneSignal/OneSignal-KMP-SDK) repository and is consumed here as a git submodule pinned to a specific commit. Its sources are checked out under `OneSignal-KMP-SDK/`, so you must initialize the submodule before building or the Gradle build will fail (the remapped `:OneSignal:logger` project directory won't exist).

```bash
# Fresh clone (recommended)
git clone --recurse-submodules git@github.com:OneSignal/OneSignal-Android-SDK.git

# If you already cloned without --recurse-submodules
git submodule update --init --recursive
```

Whenever you pull changes that move the submodule pointer, re-run `git submodule update --init --recursive` to sync your local checkout to the pinned commit.

### Working on the shared `:logger` module

The `:logger` sources live in the KMP repo and are checked out here, nested, at `OneSignal-KMP-SDK/`. This means **two gits are in play**:

- Commands run from the **Android repo root** act on Android and only record the submodule *pointer* (a `160000` gitlink SHA) — not the logger source.
- Commands run from **inside `OneSignal-KMP-SDK/`** act on the KMP repo — that's where the actual `.kt` changes are committed.

Because Gradle reads the module from the submodule folder on disk, your **uncommitted** KMP edits compile straight into the Android app — no push or version bump is needed while iterating.

There are two distinct flows: everyday development (Flow A) and releasing (Flow B).

#### Flow A — Local development (edit → commit → push → merge)

Ends when your change is on KMP `main`. **No tagging and nothing ships to Android here.**

```mermaid
flowchart TD
A["Clone with --recurse-submodules<br/>(or: git submodule update --init --recursive)"] --> B["cd OneSignal-KMP-SDK<br/>git checkout main && git pull<br/>git checkout -b feat/my-logger-change"]
B --> C["Edit :logger sources"]
C --> D["Build/verify locally (no push)<br/>Android: ./gradlew :OneSignal:core:compileDebugKotlin<br/>KMP tests: ./gradlew :logger:testDebugUnitTest :logger:iosSimulatorArm64Test spotlessCheck"]
D -->|iterate| C
D --> E["Inside the submodule: commit + push<br/>cd OneSignal-KMP-SDK<br/>git add -A && git commit -m 'feat(logger): ...'<br/>git push -u origin feat/my-logger-change"]
E --> F["Open KMP PR — CI runs (Spotless + unit tests)"]
F --> G["Merge to KMP main"]
G --> H(["Change is on KMP main — still NO tag"])
```

> The submodule starts in **detached HEAD** at the pinned commit. Run `git checkout main` (or `-b <feature>`) inside `OneSignal-KMP-SDK/` before committing, or your commit will be orphaned.

#### Flow B — Release & pin Android to a tag

Run this when a merged KMP `main` should actually ship into the Android (and later iOS) SDK. **Manual trigger**; Android itself is never tagged — only its submodule *pointer* moves to the KMP tag.

```mermaid
flowchart TD
A["KMP main has the change(s) to release"] --> B["KMP repo → Actions → 'Release' → Run workflow<br/>pick bump: patch / minor / major"]
B --> C["Release workflow (automatic):<br/>verify → compute next vX.Y.Z<br/>→ create tag + GitHub Release (auto notes)<br/>→ call Android bump workflow (GH_PUSH_TOKEN)"]
C --> D["Android PR opens: 'bump submodule → vX.Y.Z'<br/>moves the pointer to the tag's commit<br/>body includes the KMP release notes · NOT auto-merged"]
D --> E["YOU: review the notes + merge the Android PR"]
E --> F["Sync local Android checkout:<br/>git pull && git submodule update --init --recursive"]
F --> G(["Android pinned to KMP tag vX.Y.Z"])
```

The only manual actions in Flow B are **Run workflow** and **merge the bump PR**; everything in between is automated.

#### Before Submitting A Bug Report
Before creating bug reports, please check this list of steps to follow.

Expand Down
1 change: 1 addition & 0 deletions OneSignal-KMP-SDK
Submodule OneSignal-KMP-SDK added at 9cf1a3
12 changes: 12 additions & 0 deletions OneSignalSDK/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ android.enableD8 = true
# Android X settings
android.useAndroidX = true

# The `logger` KMP module uses a newer AGP than the Kotlin MPP plugin has been
# tested against; the combination works for our targets, so silence the warning.
kotlin.mpp.androidGradlePluginCompatibility.nowarn = true

# Opt in to publishing the shared KMP modules (e.g. :kmp, sourced from the
# OneSignal-KMP-SDK submodule) to Maven Central as part of the Android SDK
# release. The shared module's build.gradle only wires up Maven publishing when
# this flag is set, so its coordinate (com.onesignal:kmp, referenced by :core's
# POM) resolves for consumers. The standalone KMP repo build and the iOS host
# (which ships an XCFramework, not a Maven artifact) never set this.
onesignalPublishSharedKmp = true

# This is the name of the SDK to use when building your project.
# This will be fed from the GitHub Actions workflow.
SDK_VERSION=5.9.7
2 changes: 2 additions & 0 deletions OneSignalSDK/onesignal/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ dependencies {

// Otel module dependency
implementation(project(':OneSignal:otel'))
// Logger module (KMP, OpenTelemetry-free) — the eventual replacement for :otel.
implementation(project(':OneSignal:logger'))
testImplementation(project(':OneSignal:testhelpers'))

testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion")
Expand Down
6 changes: 3 additions & 3 deletions OneSignalSDK/onesignal/core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!-- Override otel module's minSdk requirement (26) since we have runtime checks -->
<!-- The otel module is only used on SDK 26+, so this is safe -->
<uses-sdk tools:overrideLibrary="com.onesignal.otel" />
<!-- Override otel/logger modules' minSdk requirement (26) since we have runtime checks -->
<!-- These modules are only used on SDK 26+, so this is safe -->
<uses-sdk tools:overrideLibrary="com.onesignal.otel, com.onesignal.logger" />

<!-- Required so the device can access the internet. -->
<uses-permission android:name="android.permission.INTERNET" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ enum class FeatureFlag(
"sdk_identity_verification",
FeatureActivationMode.IMMEDIATE
),

/**
* Routes SDK observability (remote logging, crash capture/upload, ANR detection) through the
* multiplatform `logger` module instead of the legacy OpenTelemetry `otel` module.
*
* APP_STARTUP: the module choice is latched for the whole process and a remote change only
* takes effect on the next app start — switching observability pipelines mid-session is unsafe.
*/
SDK_CUSTOM_LOGGING(
"sdk_custom_logging",
FeatureActivationMode.APP_STARTUP
),
;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ internal class FeatureManager(
// SDK_IDENTITY_VERIFICATION has no side effect: IdentityVerificationService
// reads featureStates directly via isEnabled() at gate-check time.
FeatureFlag.SDK_IDENTITY_VERIFICATION -> {}
// SDK_CUSTOM_LOGGING has no side effect here: the observability module choice is
// made during early init (before this manager exists) by reading the cached flag
// straight from prefs via LoggerModuleSwitch/OtelIdResolver.
FeatureFlag.SDK_CUSTOM_LOGGING -> {}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ import com.onesignal.common.threading.OneSignalDispatchers
import com.onesignal.core.internal.application.IApplicationService
import com.onesignal.core.internal.features.IFeatureManager
import com.onesignal.core.internal.startup.IStartableService
import com.onesignal.debug.internal.logging.logger.LoggerModuleSwitch
import com.onesignal.debug.internal.logging.logger.android.AndroidLogger
import com.onesignal.debug.internal.logging.logger.android.FileLogStore
import com.onesignal.debug.internal.logging.logger.android.OneSignalLogHttpSender
import com.onesignal.debug.internal.logging.logger.android.createAndroidLoggerPlatformProvider
import com.onesignal.debug.internal.logging.otel.android.AndroidOtelLogger
import com.onesignal.debug.internal.logging.otel.android.createAndroidOtelPlatformProvider
import com.onesignal.logger.LoggerFactory
import com.onesignal.otel.OtelFactory
import com.onesignal.otel.crash.OtelCrashUploader

Expand Down Expand Up @@ -34,7 +40,7 @@ internal class OneSignalCrashUploaderWrapper(
private val applicationService: IApplicationService,
private val featureManager: IFeatureManager,
) : IStartableService {
private val uploader: OtelCrashUploader by lazy {
private val otelUploader: OtelCrashUploader by lazy {
// Create Android-specific platform provider (injects Android values + a FeatureManager
// supplier that resolves to the constructor-injected manager on each access).
val platformProvider = createAndroidOtelPlatformProvider(
Expand All @@ -46,12 +52,25 @@ internal class OneSignalCrashUploaderWrapper(
OtelFactory.createCrashUploader(platformProvider, logger)
}

private val loggerUploader by lazy {
val platformProvider = createAndroidLoggerPlatformProvider(applicationService.appContext) { featureManager }
val logger = AndroidLogger()
val httpSender = OneSignalLogHttpSender(logger) { platformProvider.isExporterLoggingEnabled }
val remote = LoggerFactory.createRemoteTelemetry(platformProvider, httpSender)
val fileStore = FileLogStore(platformProvider.crashStoragePath)
LoggerFactory.createCrashUploader(platformProvider, remote, fileStore, logger)
}

@Suppress("TooGenericExceptionCaught")
override fun start() {
if (!OtelSdkSupport.isSupported) return
OneSignalDispatchers.launchOnIO {
try {
uploader.start()
if (LoggerModuleSwitch.useLoggerModule(applicationService.appContext)) {
loggerUploader.start()
} else {
otelUploader.start()
}
} catch (t: Throwable) {
com.onesignal.debug.internal.logging.Logging.warn(
"OneSignal: Crash uploader failed to start: ${t.message}",
Expand Down
Loading
Loading