Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 66 additions & 0 deletions .github/workflows/sbom.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Release SBOM

# Generates the CycloneDX superset SBOM for each release, attaches it (plus a
# source archive) to the GitHub release, and publishes a signed attestation
# binding the SBOM to that archive. Can also be run manually to inspect the
# SBOM for the current tree (uploaded as a workflow artifact instead).

on:
release:
types: [published]
workflow_dispatch:

permissions:
contents: write
id-token: write
attestations: write

jobs:
sbom:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: "recursive"

- name: Generate CycloneDX SBOM
id: generate
run: |
python3 scripts/generate_sbom.py
SBOM_FILE=$(ls countly-sdk-cpp-*.cdx.json)
echo "sbom_file=$SBOM_FILE" >> "$GITHUB_OUTPUT"
VERSION=$(echo "$SBOM_FILE" | sed 's/countly-sdk-cpp-\(.*\)\.cdx\.json/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Create source archive
run: |
git archive --format=tar.gz \
--prefix="countly-sdk-cpp-${{ steps.generate.outputs.version }}/" \
-o "countly-sdk-cpp-${{ steps.generate.outputs.version }}.tar.gz" HEAD

- name: Attest SBOM
uses: actions/attest-sbom@v2
with:
subject-path: countly-sdk-cpp-${{ steps.generate.outputs.version }}.tar.gz
sbom-path: ${{ steps.generate.outputs.sbom_file }}

- name: Attach to release
if: github.event_name == 'release'
run: |
gh release upload "${{ github.event.release.tag_name }}" \
"${{ steps.generate.outputs.sbom_file }}" \
"countly-sdk-cpp-${{ steps.generate.outputs.version }}.tar.gz" \
--clobber
env:
GH_TOKEN: ${{ github.token }}

- name: Upload as workflow artifact (manual runs)
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: sbom
path: |
${{ steps.generate.outputs.sbom_file }}
countly-sdk-cpp-${{ steps.generate.outputs.version }}.tar.gz
39 changes: 35 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ jobs:
matrix:
os: [ubuntu-22.04, ubuntu-24.04, macos-15, windows-2022]
sqlite: [OFF, ON]
exclude:
# Windows has no system sqlite3.lib
- os: windows-2022
sqlite: ON
# Windows + SQLite is now supported: the vendored amalgamation is
# compiled into the library, no system sqlite3.lib needed.

steps:
- name: Checkout code
Expand Down Expand Up @@ -209,6 +207,39 @@ jobs:
- name: Run tests
run: cd build && ./countly-tests

# ──────────────────────────────────────────────
# System SQLite (COUNTLY_USE_SYSTEM_SQLITE escape hatch)
# ──────────────────────────────────────────────
system-sqlite:
runs-on: ubuntu-24.04
timeout-minutes: 120
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: "recursive"

- name: Install dependencies
run: |
sudo apt-get update && sudo apt-get install -y \
libcurl4-openssl-dev \
libssl-dev \
libsqlite3-dev

- name: Configure with system SQLite
run: |
cmake -DCOUNTLY_BUILD_TESTS=1 -DCOUNTLY_USE_SQLITE=ON \
-DCOUNTLY_USE_SYSTEM_SQLITE=ON \
-B build .
env:
CMAKE_POLICY_VERSION_MINIMUM: "3.31"

- name: Build
run: cd build && make ./countly-tests

- name: Run tests
run: cd build && ./countly-tests

# ──────────────────────────────────────────────
# C++17 compatibility check
# ──────────────────────────────────────────────
Expand Down
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[submodule "vendor/sqlite"]
path = vendor/sqlite
url = https://github.com/azadkuh/sqlite-amalgamation.git
[submodule "vendor/doctest"]
path = vendor/doctest
url = https://github.com/onqtam/doctest.git
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
## X.X.X
- ! Minor breaking change ! Added SDK internal limits enforcement (max key length, value size, segmentation values, breadcrumb count, stack-trace lines per thread, stack-trace line length) across events, views, crashes, and user properties. Limits use config defaults overridable by server SDK Behavior Settings, and can be set via `setMaxKeyLength`, `setMaxValueSize`, `setMaxSegmentationValues`, `setMaxBreadcrumbCount`, `setMaxStackTraceLinesPerThread`, `setMaxStackTraceLineLength` during init.

- Added multi-instance support: several `Countly` instances can now run in one process, each with its own app key, queues, storage, and threads. Instances can be owned by the integrator, or created and looked up by name with `createInstance`, `getInstance(name)`, `findInstance`, `hasInstance`, `destroyInstance` and `destroyAllInstances`.
- ! Minor breaking change ! When built with SQLite, each instance requires its own database path. A second instance claiming a path already in use logs an error and does not initialize.
- Fixed the libcurl global lifecycle: `curl_global_init` now runs once per process, and cleanup no longer runs when an instance is destroyed, which could tear down networking underneath another live instance. Added `shutdownNetworking()` for hosts that load and unload the SDK without exiting.
- Fixed non-unique event and view IDs: the random component of generated IDs was constant for the lifetime of the process, and on platforms with a coarse `system_clock` (Windows, ~15ms) the timestamp component did not change either, so IDs generated within one tick were identical.
- Fixed a remote config fetch outliving the objects it used: the fetch no longer holds any reference to the SDK instance, only to the modules and the value store it needs, each kept alive for as long as the fetch runs. Neither the calling thread nor destruction ever waits for it. Only one fetch runs at a time per instance: a call made while a fetch is in flight is logged and ignored rather than queued, so `updateRemoteConfig`, `updateRemoteConfigFor` and `updateRemoteConfigExcept` never block the calling thread. `shutdownNetworking()` now also refuses while a fetch is in flight.
- Calling the SDK from the log callback no longer hangs or recurses without bound. Log messages raised while a thread is inside the callback are dropped, `checkRQSize()` no longer takes the instance mutex (the storage modules serialize their own access), and the event queue size getters report -1 instead of deadlocking when called from the callback.
- Fixed dropped events and requests on SQLite builds: no busy timeout was set on any database connection, so a write that overlapped a read from another thread failed immediately with `SQLITE_BUSY` and the event or request was discarded with only an error log. Every connection now waits for the lock.
- Fixed `endSession` checking whether a session was active without holding the instance mutex, so two concurrent calls could both send an `end_session` request.
- Fixed the event queue flush when the queue is emptied by another thread mid-flush: it queued a request with an empty event list, and on SQLite builds it also ran a malformed `DELETE ... WHERE evtid IN )` statement that failed with a SQLite syntax error.
- Fixed `createEventTableSchema` and `debugReturnStateOfEQ` falling off the end of a non-void function when a `std::system_error` was caught; in the first case the indeterminate value decided whether the SDK considered itself initialized.
- Fixed a data race in the views module: the open-view map and the first-view flag were kept without a lock, so opening or closing views from more than one thread could lose view events, report `start` on more than one view, or corrupt the map. View recording still happens outside that lock, so a log callback that calls back into the SDK cannot deadlock.
- Fixed a data race in the `dow`, `hour` and `tz` fields of events and requests: they were derived with `std::localtime` and `std::gmtime`, which share one process-wide buffer, so concurrent recording (each instance runs its own update loop) could report another thread's or the other function's values. Both now use the reentrant variants.
- Fixed a lost wakeup when stopping the periodic SDK Behavior Settings timer: the stop flag was set without holding the mutex the timer thread waits on, so the notification could be missed and the joining thread could block for up to the full four-hour update interval.

## 26.1.1
- Updated CMake minimum required version to use the range format with upper the end of `3.31`.
- Hardened mutex handling against exceptions.
Expand Down
40 changes: 31 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ option(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS "Create a module definition (.def) on W
option(COUNTLY_USE_CUSTOM_HTTP "Use a custom HTTP library" OFF)
option(COUNTLY_USE_CUSTOM_SHA256 "Use a custom SHA 256 library" OFF)
option(COUNTLY_USE_SQLITE "Use SQLite" OFF)
option(COUNTLY_USE_SYSTEM_SQLITE "Link the system SQLite instead of compiling the vendored amalgamation" OFF)
option(COUNTLY_BUILD_TESTS "Build test programs" OFF)
option(COUNTLY_BUILD_SAMPLE "Build Sample programs" OFF)

Expand All @@ -21,6 +22,7 @@ message("Create a module definition (.def) on Windows.:" ${CMAKE_WINDOWS_EXPORT_
message("Use a custom HTTP library:" ${COUNTLY_USE_CUSTOM_HTTP})
message("Use a custom SHA 256 library:" ${COUNTLY_USE_CUSTOM_SHA256})
message("Use SQLite:" ${COUNTLY_USE_SQLITE})
message("Use system SQLite:" ${COUNTLY_USE_SYSTEM_SQLITE})
message("Build test programs:" ${COUNTLY_BUILD_TESTS})
message("Build Sample programs:" ${COUNTLY_BUILD_SAMPLE})

Expand Down Expand Up @@ -97,12 +99,23 @@ elseif(NOT WIN32)
endif()

if(COUNTLY_USE_SQLITE)
message("Building sqlite directories")
target_compile_definitions(countly PRIVATE COUNTLY_USE_SQLITE)
set(Sqlite3_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite/build)
find_package(Sqlite3)
target_include_directories(countly PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite)
target_link_libraries(countly sqlite3)
if(COUNTLY_USE_SYSTEM_SQLITE)
message("Linking system SQLite")
find_package(SQLite3 REQUIRED)
target_link_libraries(countly SQLite::SQLite3)
else()
message("Compiling vendored SQLite amalgamation")
enable_language(C)
target_sources(countly PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite3/sqlite3.c)
target_include_directories(countly PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite3)
# No loadable-extension support: not needed by the SDK, avoids linking
# libdl and removes the extension attack surface.
target_compile_definitions(countly PRIVATE SQLITE_OMIT_LOAD_EXTENSION SQLITE_OMIT_DEPRECATED SQLITE_DEFAULT_MEMSTATUS=0)
if(NOT WIN32)
target_link_libraries(countly m)
endif()
endif()
endif()

if(COUNTLY_BUILD_TESTS)
Expand All @@ -122,14 +135,19 @@ if(COUNTLY_BUILD_TESTS)
${CMAKE_CURRENT_SOURCE_DIR}/tests/mutex_exception_safety.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/sbs.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits_integration.cpp)
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits_integration.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/multi_instance.cpp)

target_compile_options(countly-tests PRIVATE -g)
target_compile_options(countly-tests PRIVATE $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-g>)
target_compile_definitions(countly-tests PRIVATE COUNTLY_BUILD_TESTS)
if(COUNTLY_USE_SQLITE)
message("Compiling definitions for sqlite")
target_compile_definitions(countly-tests PRIVATE COUNTLY_USE_SQLITE)
target_include_directories(countly-tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite)
if(COUNTLY_USE_SYSTEM_SQLITE)
target_link_libraries(countly-tests SQLite::SQLite3)
else()
target_include_directories(countly-tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite3)
endif()
endif()
if(COUNTLY_USE_CUSTOM_SHA256)
message("Compiling definitions for custom sha256")
Expand All @@ -152,7 +170,11 @@ if(COUNTLY_BUILD_SAMPLE)
if(COUNTLY_USE_SQLITE)
message("Compiling definitions for sqlite")
target_compile_definitions(countly-sample PRIVATE COUNTLY_USE_SQLITE)
target_include_directories(countly-sample PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite)
if(COUNTLY_USE_SYSTEM_SQLITE)
target_link_libraries(countly-sample SQLite::SQLite3)
else()
target_include_directories(countly-sample PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sqlite3)
endif()
endif()
target_include_directories(countly-sample PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/doctest/doctest)
target_include_directories(countly-sample PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/json/include)
Expand Down
23 changes: 23 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
# Security Policy

Security is very important to us. If you discover any issue regarding security, please disclose the information responsibly by sending an email to security@count.ly and not by creating a GitHub issue.

## Software Bill of Materials (SBOM)

Each GitHub release includes a CycloneDX 1.6 SBOM
(`countly-sdk-cpp-<version>.cdx.json`) with a signed attestation binding it to
the release source archive. You can also generate one for any checkout with
`python3 scripts/generate_sbom.py`, or export an SPDX SBOM of the source tree
via GitHub (Insights → Dependency graph → Export SBOM).

Because this SDK is distributed as source and its dependency set varies with
CMake options, the SBOM is a **superset**: every component the SDK can pull in
is listed, and each component's `scope` and `countly:*` properties state which
CMake option includes it.

- `required` components (e.g. nlohmann/json) are always part of the SDK.
- `optional` components are gated by a CMake option named in the
`countly:cmake-option` property (e.g. SQLite via `COUNTLY_USE_SQLITE`).
- `excluded` components (e.g. doctest) are used only for testing and are never
part of the shipped library.
- System-resolved libraries (OpenSSL, curl, system SQLite) carry **no version**
by design: their versions are determined by your build environment, so their
vulnerability exposure belongs to your build's own SBOM.

Loading
Loading