diff --git a/.github/scripts/public_api.py b/.github/scripts/public_api.py new file mode 100644 index 0000000..f13a3f7 --- /dev/null +++ b/.github/scripts/public_api.py @@ -0,0 +1,93 @@ +"""Prints the SDK's public C++ API as sorted " " lines, from clang's AST.""" +import json +import sys + +RECORDS = {"CXXRecordDecl", "ClassTemplateDecl"} +MEMBERS = { + "CXXMethodDecl": "method", + "CXXConstructorDecl": "constructor", + "CXXDestructorDecl": "destructor", + "CXXConversionDecl": "conversion", + "FieldDecl": "field", + "VarDecl": "variable", + "FunctionDecl": "function", + "FunctionTemplateDecl": "function template", + "TypedefDecl": "typedef", + "TypeAliasDecl": "alias", +} + + +def documents(text): + """Yields each JSON document in text: clang's -ast-dump-filter writes one per matching declaration.""" + decoder = json.JSONDecoder() + position = 0 + while True: + while position < len(text) and text[position].isspace(): + position += 1 + if position >= len(text): + return + document, position = decoder.raw_decode(text, position) + yield document + + +def signature(node): + """Returns the type of a declaration with the flags that change its contract, such as virtual.""" + if node["kind"] == "FunctionTemplateDecl": + node = next((child for child in node.get("inner", []) + if child.get("kind") in ("FunctionDecl", "CXXMethodDecl")), node) + flags = [flag for flag in ("virtual", "pure", "explicit", "constexpr") if node.get(flag)] + if node.get("storageClass") == "static": + flags.append("static") + return " ".join([node.get("type", {}).get("qualType", "")] + flags).strip() + + +def walk(node, scope, access, lines): + """Adds the public declarations under node, declared in scope with the given default member access.""" + kind = node.get("kind") + if node.get("isImplicit"): + return + member_access = node.get("access", access) + if kind == "NamespaceDecl": + for child in node.get("inner", []): + walk(child, f"{scope}{node.get('name', '')}::", "public", lines) + return + if member_access not in ("public", "protected"): + return + name = node.get("name") + if kind in RECORDS and name: + record = node + if kind == "ClassTemplateDecl": + record = next((child for child in node.get("inner", []) + if child.get("kind") == "CXXRecordDecl"), {}) + if not record.get("completeDefinition"): + return + lines.add(f"{scope}{name} {record.get('tagUsed', 'class')} {member_access}") + section = "public" if record.get("tagUsed") == "struct" else "private" + for child in record.get("inner", []): + if child.get("kind") == "AccessSpecDecl": + section = child.get("access", section) + else: + walk(child, f"{scope}{name}::", section, lines) + elif kind == "EnumDecl" and name: + lines.add(f"{scope}{name} enum {member_access}") + for constant in node.get("inner", []): + if constant.get("kind") == "EnumConstantDecl": + lines.add(f"{scope}{name}::{constant['name']} enumerator {member_access}") + elif kind in MEMBERS and name: + lines.add(f"{scope}{name} {MEMBERS[kind]} {signature(node)} {member_access}") + + +def main(): + """Reads the JSON AST of include/countly.hpp, filtered to "cly", from stdin and prints the public API lines.""" + # A set, because a declaration can arrive in more than one document. + lines = set() + for document in documents(sys.stdin.read()): + if document.get("kind") == "NamespaceDecl" and document.get("name") == "cly": + walk(document, "", "public", lines) + if not lines: + sys.exit("No declarations in namespace cly were found in the AST.") + print("\n".join(sorted(lines))) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/public_api_record.sh b/.github/scripts/public_api_record.sh new file mode 100644 index 0000000..88f5c95 --- /dev/null +++ b/.github/scripts/public_api_record.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Compares the public API this run extracted with the record committed in the repository. On a difference it lists the +# changed lines in the job summary, copies the extracted API to $RUNNER_TEMP/public-api-record/ under the record's path, +# for the workflow to upload, and fails: a deliberate change is committed together with a changelog line. +# $@ pairs "=" +set -euo pipefail +out="$RUNNER_TEMP/public-api-record" +status=0 +current="$RUNNER_TEMP/public-api-current.txt" +recorded="$RUNNER_TEMP/public-api-recorded.txt" +for pair in "$@"; do + extracted=${pair%%=*} + record=${pair#*=} + # Line endings do not count: .NET writes CRLF on Windows, and a Windows checkout can give the record CRLF too. + tr -d '\r' < "$extracted" > "$current" + if [ -f "$record" ] && tr -d '\r' < "$record" | cmp -s - "$current"; then + echo "The public API is the one $record records." + continue + fi + status=1 + mkdir -p "$out/$(dirname "$record")" + cp "$current" "$out/$record" + { + printf '### The public API differs from %s\n\n' "$record" + printf "Commit \`%s\` from the \`public-api-record\` artifact of this run, with a changelog line for the change.\n\n" "$record" + if [ -f "$record" ]; then + printf '```diff\n' + tr -d '\r' < "$record" > "$recorded" + diff -u "$recorded" "$current" | tail -n +3 | head -n 300 || true + printf '```\n\n' + else + printf 'There is no record yet.\n\n' + fi + } >> "$GITHUB_STEP_SUMMARY" + echo "::error title=Public API record::The public API differs from $record. Commit the file from this run's public-api-record artifact and add a changelog line." +done +exit "$status" diff --git a/.github/scripts/public_api_report.sh b/.github/scripts/public_api_report.sh new file mode 100644 index 0000000..e9b930c --- /dev/null +++ b/.github/scripts/public_api_report.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Writes the report of a pull request's public API changes to the job summary, and to the step output "report", which +# the workflow's comment job posts on the pull request. The comment lists at most 200 lines per section. +# $1 a file with one removed or changed declaration per line +# $2 a file with one declaration per line that the pull request adds to the public API +set -euo pipefail +breaking=$1 +added=$2 +marker='' + +# Prints a section titled $1 and explained by $2 with the lines of file $3, at most $4 of them when $4 is not 0. +section() { + local count + count=$(grep -c . "$3" || true) + [ "$count" -gt 0 ] || return 0 + printf '**%s: %s.** %s\n\n```\n' "$1" "$count" "$2" + if [ "$4" -gt 0 ]; then + head -n "$4" "$3" + else + cat "$3" + fi + printf '```\n' + if [ "$4" -gt 0 ] && [ "$count" -gt "$4" ]; then + printf '\n%s more in the job summary.\n' "$((count - $4))" + fi + printf '\n' +} + +# Prints the whole report, with at most $1 lines per section when $1 is not 0. +report() { + printf '%s\n### Public API changes\n\n' "$marker" + if ! grep -q . "$breaking" && ! grep -q . "$added"; then + printf 'No public API change.\n' + return + fi + section 'Removed or changed' 'Each needs a CHANGELOG entry that marks it as a breaking change.' "$breaking" "$1" + section 'Added' 'Check that each one is meant to be public.' "$added" "$1" +} + +report 0 >> "$GITHUB_STEP_SUMMARY" +delimiter="EOF_$(od -An -N8 -tx1 /dev/urandom | tr -d ' \n')" +{ + echo "report<<$delimiter" + report 200 + echo "$delimiter" +} >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/api-changelog.yml b/.github/workflows/api-changelog.yml new file mode 100644 index 0000000..e2041e6 --- /dev/null +++ b/.github/workflows/api-changelog.yml @@ -0,0 +1,53 @@ +name: Public API changelog + +# A pull request that changes or removes a record of the public API, or of the requests the SDK sends, must also change +# the CHANGELOG.md that API_FILES pairs it with, so that the release notes tell integrators about every such change. A +# record the pull request adds needs no line. A path ending in "/" stands for every file under it. The API and request +# checks keep each record in step with the SDK. +on: + pull_request: + branches: [master, staging] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + pull-requests: read # to list the files the pull request changes + +jobs: + changelog: + name: A public API or request change comes with a CHANGELOG line + runs-on: ubuntu-24.04 + steps: + - name: Check the files the pull request changes + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + API_FILES: api/countly-sdk-cpp.api=CHANGELOG.md api/requests/=CHANGELOG.md + run: | + changed=$(gh api "repos/$REPO/pulls/$PR/files" --paginate --jq '.[] | select(.status != "added") | .filename') + changes() { + case "$1" in + */) printf '%s\n' "$changed" | awk -v prefix="$1" 'index($0, prefix) == 1 { found = 1 } END { exit !found }' ;; + *) printf '%s\n' "$changed" | grep -qxF "$1" ;; + esac + } + recorded_changed=no + for pair in $API_FILES; do + record=${pair%%=*} + changelog=${pair#*=} + if changes "$record"; then + recorded_changed=yes + if ! changes "$changelog"; then + echo "::error::This pull request changes $record, which records the public API or the requests the SDK sends, but not $changelog. Add a line that tells integrators what changed." + exit 1 + fi + fi + done + if [ "$recorded_changed" = yes ]; then + echo "The recorded public API or requests change, and the CHANGELOG changes with them." + else + echo "This pull request changes neither the recorded public API nor the recorded requests." + fi diff --git a/.github/workflows/public_api.yml b/.github/workflows/public_api.yml new file mode 100644 index 0000000..ed3e596 --- /dev/null +++ b/.github/workflows/public_api.yml @@ -0,0 +1,100 @@ +name: Public API changes + +on: + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + api: + name: Breaking changes + outputs: + report: ${{ steps.report.outputs.report }} + runs-on: ubuntu-24.04 + steps: + - name: Checkout SDK code + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + fetch-depth: 2 + submodules: "recursive" + persist-credentials: false + + # The first parent of the pull request's merge commit is the base branch it merges into. + # Both sides read the vendored JSON headers of this pull request: a worktree has no submodules checked out. + - name: Compare the public API with the base branch + id: compare + run: | + git worktree add "$RUNNER_TEMP/base" HEAD^1 + for side in base head; do + tree="$GITHUB_WORKSPACE" + if [ "$side" = base ]; then + tree="$RUNNER_TEMP/base" + fi + clang++-18 -x c++ -std=c++14 -fsyntax-only -I"$tree/include" -I"$GITHUB_WORKSPACE/vendor/json/include" \ + -Xclang -ast-dump=json -Xclang -ast-dump-filter=cly "$tree/include/countly.hpp" \ + | python3 .github/scripts/public_api.py > "$RUNNER_TEMP/$side.txt" + done + LC_ALL=C comm -23 "$RUNNER_TEMP/base.txt" "$RUNNER_TEMP/head.txt" | sed 's/^/Removed or changed: /' > "$RUNNER_TEMP/breaking.txt" + LC_ALL=C comm -13 "$RUNNER_TEMP/base.txt" "$RUNNER_TEMP/head.txt" > "$RUNNER_TEMP/added.txt" + + - name: Report the public API changes + id: report + run: bash .github/scripts/public_api_report.sh "$RUNNER_TEMP/breaking.txt" "$RUNNER_TEMP/added.txt" + + - name: Require a CHANGELOG entry for breaking changes + run: | + if [ ! -s "$RUNNER_TEMP/breaking.txt" ]; then + exit 0 + fi + if git diff --quiet HEAD^1 HEAD -- CHANGELOG.md; then + echo "::error title=Breaking API change::This pull request breaks the public API without a CHANGELOG.md entry. The job summary lists each break." + exit 1 + fi + echo "::warning title=Breaking API change::This pull request breaks the public API. Check that CHANGELOG.md marks it as a breaking change." + + # The committed record keeps every change to the public API visible in the pull request's diff. + - name: Compare the public API with its record + id: record + if: ${{ !cancelled() && steps.compare.outcome == 'success' }} + run: > + bash .github/scripts/public_api_record.sh + "$RUNNER_TEMP/head.txt=api/countly-sdk-cpp.api" + + - name: Upload the new record + if: ${{ !cancelled() && steps.record.outcome == 'failure' }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: public-api-record + path: ${{ runner.temp }}/public-api-record + + comment: + name: Comment the public API changes + needs: api + # A fork's or Dependabot's pull request only gets a read-only token, so its report stays in the job summary. + if: >- + always() && needs.api.outputs.report != '' && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]' + runs-on: ubuntu-24.04 + permissions: + pull-requests: write + steps: + - name: Post or update the comment + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + REPORT: ${{ needs.api.outputs.report }} + run: | + id=$(gh api --paginate "repos/$REPO/issues/$PR/comments" --jq \ + '.[] | select(.user.login == "github-actions[bot]" and (.body | startswith(""))) | .id' | tail -n 1) + if [ -n "$id" ]; then + gh api --method PATCH "repos/$REPO/issues/comments/$id" -f "body=$REPORT" > /dev/null + elif ! grep -qx 'No public API change.' <<< "$REPORT"; then + gh api --method POST "repos/$REPO/issues/$PR/comments" -f "body=$REPORT" > /dev/null + fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 222462c..14fdfbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -174,10 +174,12 @@ if(COUNTLY_BUILD_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/tests/remote_config.cpp ${CMAKE_CURRENT_SOURCE_DIR}/tests/crash_reporting.cpp ${CMAKE_CURRENT_SOURCE_DIR}/tests/location.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/tests/server_url_app_key.cpp) + ${CMAKE_CURRENT_SOURCE_DIR}/tests/server_url_app_key.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/tests/request_scenarios.cpp) target_compile_options(countly-tests PRIVATE $<$>:-g>) - target_compile_definitions(countly-tests PRIVATE COUNTLY_BUILD_TESTS) + # the request scenarios read and write their records under api/requests/ + target_compile_definitions(countly-tests PRIVATE COUNTLY_BUILD_TESTS COUNTLY_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") if(COUNTLY_USE_SQLITE) if(COUNTLY_USE_SYSTEM_SQLITE) target_link_libraries(countly-tests SQLite::SQLite3) diff --git a/api/countly-sdk-cpp.api b/api/countly-sdk-cpp.api new file mode 100644 index 0000000..bebb85b --- /dev/null +++ b/api/countly-sdk-cpp.api @@ -0,0 +1,408 @@ +cly::ConfigurationModule class public +cly::ConfigurationModule::ConfigurationModule constructor void (cly::CountlyDelegates *, std::shared_ptr, std::shared_ptr, std::shared_ptr, std::shared_ptr, std::shared_ptr, std::shared_ptr) public +cly::ConfigurationModule::fetchConfigFromServer method void (nlohmann::json) public +cly::ConfigurationModule::fetchConfigFromStorage method void () public +cly::ConfigurationModule::getAutomaticSessionTracking method int () const public +cly::ConfigurationModule::getBackoffAcceptedTimeout method unsigned int () const public +cly::ConfigurationModule::getBackoffDuration method unsigned int () const public +cly::ConfigurationModule::getBackoffRequestAge method unsigned int () const public +cly::ConfigurationModule::getBackoffRequestQueuePercentage method double () const public +cly::ConfigurationModule::getConsentRequired method int () const public +cly::ConfigurationModule::getEventFilterList method FilterList> () const public +cly::ConfigurationModule::getEventQueueSizeLimit method unsigned int () public +cly::ConfigurationModule::getEventSegmentationFilterList method FilterList>> () const public +cly::ConfigurationModule::getLimits method SDKLimits () const public +cly::ConfigurationModule::getRequestDropAgeHours method unsigned int () const public +cly::ConfigurationModule::getRequestQueueSizeLimit method unsigned int () const public +cly::ConfigurationModule::getSegmentationFilterList method FilterList> () const public +cly::ConfigurationModule::getServerLocationTracking method int () const public +cly::ConfigurationModule::getSessionUpdateInterval method unsigned int () public +cly::ConfigurationModule::getUserPropertyFilterList method FilterList> () const public +cly::ConfigurationModule::isBackoffEnabled method bool () const public +cly::ConfigurationModule::isCrashReportingEnabled method bool () const public +cly::ConfigurationModule::isCustomEventTrackingEnabled method bool () public +cly::ConfigurationModule::isLocationTrackingEnabled method bool () public +cly::ConfigurationModule::isNetworkingEnabled method bool () const public +cly::ConfigurationModule::isSessionTrackingEnabled method bool () public +cly::ConfigurationModule::isTrackingEnabled method bool () const public +cly::ConfigurationModule::isViewTrackingEnabled method bool () const public +cly::ConfigurationModule::startServerConfigUpdateTimer method void (nlohmann::json) public +cly::ConfigurationModule::stopTimer method void () public +cly::ConfigurationModule::~ConfigurationModule destructor void () noexcept public +cly::ConfigurationProvider class public +cly::ConfigurationProvider::getBackoffAcceptedTimeout method unsigned int () const virtual public +cly::ConfigurationProvider::getBackoffDuration method unsigned int () const virtual public +cly::ConfigurationProvider::getBackoffRequestAge method unsigned int () const virtual public +cly::ConfigurationProvider::getBackoffRequestQueuePercentage method double () const virtual public +cly::ConfigurationProvider::getLimits method SDKLimits () const virtual pure public +cly::ConfigurationProvider::getRequestDropAgeHours method unsigned int () const virtual public +cly::ConfigurationProvider::getRequestQueueSizeLimit method unsigned int () const virtual pure public +cly::ConfigurationProvider::isBackoffEnabled method bool () const virtual public +cly::ConfigurationProvider::isCrashReportingEnabled method bool () const virtual pure public +cly::ConfigurationProvider::isNetworkingEnabled method bool () const virtual pure public +cly::ConfigurationProvider::isTrackingEnabled method bool () const virtual pure public +cly::ConfigurationProvider::isViewTrackingEnabled method bool () const virtual pure public +cly::ConfigurationProvider::~ConfigurationProvider destructor void () noexcept virtual public +cly::ConsentModule class public +cly::ConsentModule::createGroup method void (const std::string &, const std::vector &) public +cly::ConsentModule::expand method std::vector (const std::vector &, std::vector &) const public +cly::ConsentModule::featuresToChange method std::vector (const std::vector &, bool) const public +cly::ConsentModule::featuresWithFlag method std::vector (bool) const public +cly::ConsentModule::getConsent method bool (const std::string &) const public +cly::ConsentModule::isFeature method bool (const std::string &) static public +cly::ConsentModule::isGroup method bool (const std::string &) const public +cly::ConsentModule::isKnownName method bool (const std::string &) const public +cly::ConsentModule::isRequired method bool () const public +cly::ConsentModule::requirementChangesWith method bool (int) const public +cly::ConsentModule::setGiven method void (const std::vector &, bool) public +cly::ConsentModule::setOnlyGiven method void (const std::vector &) public +cly::ConsentModule::setRequiredByApp method void (bool) public +cly::ConsentModule::setRequiredByServer method void (int) public +cly::ConsentModule::state method nlohmann::json () const public +cly::Countly class public +cly::Countly::AutomaticSessionTrackingChanged method void () public +cly::Countly::ConsentRequirementChanged method void () public +cly::Countly::Countly constructor void () public +cly::Countly::Countly constructor void (Countly &&) public +cly::Countly::Countly constructor void (const Countly &) public +cly::Countly::CurrentViewId method std::string () public +cly::Countly::DeviceMetrics method nlohmann::json () public +cly::Countly::IsSessionRunning method bool () public +cly::Countly::LocationSettingChanged method void () public +cly::Countly::PlatformName method std::string () public +cly::Countly::RecordEvent method void (const std::string &, const std::map &, int) public +cly::Countly::RecordEvent method void (const std::string &, const std::map &, int, double) public +cly::Countly::RecordEvent method void (const std::string &, const std::map &, int, double, double) public +cly::Countly::RecordEvent method void (const std::string &, int) public +cly::Countly::RecordEvent method void (const std::string &, int, double) public +cly::Countly::RecordLocation method void (const std::string &, const std::string &, const std::string &, const std::string &) public +cly::Countly::SetMaxEventsPerMessage method void (int) public +cly::Countly::SetMetrics method void (const std::string &, const std::string &, const std::string &, const std::string &, const std::string &, const std::string &) public +cly::Countly::SetMinUpdatePeriod method void (int) public +cly::Countly::SetPath method void (const std::string &) public +cly::Countly::Start method void (const std::string &, const std::string &, int) public +cly::Countly::StartOnCloud method void (const std::string &) public +cly::Countly::Stop method void () public +cly::Countly::addEvent method void (const cly::Event &) public +cly::Countly::addEventToSqlite method void (const cly::Event &) public +cly::Countly::alwaysUsePost method void (bool) public +cly::Countly::attemptSessionUpdateEQ method bool () public +cly::Countly::attemptToSendStoredRequests method void () public +cly::Countly::beginSession method bool () public +cly::Countly::calculateChecksum method std::string (const std::string &, const std::string &) public +cly::Countly::changeAppKey method void (const std::string &) public +cly::Countly::changeServerUrl method void (const std::string &, int) public +cly::Countly::checkAllConsent method void () public +cly::Countly::checkAndSendEventToRQ method void () public +cly::Countly::checkEQSize method int () public +cly::Countly::checkMemoryEQSize method int () public +cly::Countly::checkPersistentEQSize method int () public +cly::Countly::checkRQSize method int () public +cly::Countly::clearEQInternal method void () public +cly::Countly::clearPersistentEQ method void () public +cly::Countly::crash method cly::CrashModule &() const public +cly::Countly::createFeatureGroup method void (const std::string &, const std::vector &) public +cly::Countly::createInstance method Countly &(const std::string &) static public +cly::Countly::destroyAllInstances method void () static public +cly::Countly::destroyInstance method void (const std::string &) static public +cly::Countly::disableAutoEventsOnUserProperties method void () public +cly::Countly::disableBackoffMechanism method void () public +cly::Countly::disableGradualRequestCleaner method void () public +cly::Countly::disableHealthCheck method void () public +cly::Countly::disableLocation method void () public +cly::Countly::disableSDKBehaviorSettingsUpdates method void () public +cly::Countly::enableImmediateRequestOnStop method void () public +cly::Countly::enableManualSessionControl method void () public +cly::Countly::enableRemoteConfig method void () public +cly::Countly::endSession method bool () public +cly::Countly::enrollABOnRCDownload method void () public +cly::Countly::fillEventsIntoJson method void (nlohmann::json &, std::string &) public +cly::Countly::findInstance method Countly *(const std::string &) static public +cly::Countly::flushEvents method void (std::chrono::seconds) public +cly::Countly::flushQueues method void () public +cly::Countly::getConsent method bool (const std::string &) public +cly::Countly::getDeviceID method std::string () public +cly::Countly::getDeviceIDType method DeviceIdType () public +cly::Countly::getInstance method Countly &() static public +cly::Countly::getInstance method Countly &(const std::string &) static public +cly::Countly::getRemoteConfigValue method nlohmann::json (const std::string &) public +cly::Countly::getTimestamp method std::chrono::system_clock::time_point () static public +cly::Countly::giveConsent method void (const std::vector &) public +cly::Countly::giveConsentAll method void () public +cly::Countly::hasInstance method bool (const std::string &) static public +cly::Countly::multiplyCustomUserDetail method void (const std::string &, double) public +cly::Countly::operator= method Countly &(Countly &&) public +cly::Countly::operator= method void (const Countly &) public +cly::Countly::removeConsent method void (const std::vector &) public +cly::Countly::removeConsentAll method void () public +cly::Countly::removeDifferentAppKeysFromQueue method void () public +cly::Countly::removeEventWithId method void (const std::string &) public +cly::Countly::replaceAllAppKeysInQueueWithCurrentAppKey method void () public +cly::Countly::sendEventsToRQ method void (const nlohmann::json &) public +cly::Countly::setAutomaticSessionUpdateInterval method void (unsigned short) public +cly::Countly::setCity method void (const std::string &) public +cly::Countly::setClearStoredDeviceID method void (bool) public +cly::Countly::setConsent method void (const std::vector &, bool) public +cly::Countly::setConsentEnabled method void (const std::vector &) public +cly::Countly::setConsentFeatureGroup method void (const std::string &, bool) public +cly::Countly::setCountry method void (const std::string &) public +cly::Countly::setCrashFilterCallback method void (CrashFilterCallback) public +cly::Countly::setCustomCrashSegmentation method void (const std::map &) public +cly::Countly::setCustomUserDetails method void (const std::map &) public +cly::Countly::setDatabasePath method void (const std::string &) public +cly::Countly::setDeviceID method void (const std::string &) public +cly::Countly::setDeviceID method void (const std::string &, bool) public +cly::Countly::setEventsToRQThreshold method void (int) public +cly::Countly::setFailedRequestCooldown method void (int) public +cly::Countly::setHTTPClient method void (HTTPClientFunction) public +cly::Countly::setLocation method void (const std::string &, const std::string &, const std::string &, const std::string &) public +cly::Countly::setLocation method void (double, double) public +cly::Countly::setLogger method void (void (*)(LogLevel, const std::string &)) public +cly::Countly::setMaxBreadcrumbCount method void (unsigned int) public +cly::Countly::setMaxEvents method void (size_t) public +cly::Countly::setMaxKeyLength method void (unsigned int) public +cly::Countly::setMaxRQProcessingBatchSize method void (unsigned int) public +cly::Countly::setMaxRequestQueueSize method void (unsigned int) public +cly::Countly::setMaxSegmentationValues method void (unsigned int) public +cly::Countly::setMaxStackTraceLineLength method void (unsigned int) public +cly::Countly::setMaxStackTraceLinesPerThread method void (unsigned int) public +cly::Countly::setMaxValueSize method void (unsigned int) public +cly::Countly::setMetrics method void (const std::string &, const std::string &, const std::string &, const std::string &, const std::string &, const std::string &) public +cly::Countly::setRequestDropAgeHours method void (int) public +cly::Countly::setRequiresConsent method void (bool) public +cly::Countly::setSDKBehaviorSettings method void (const std::string &) public +cly::Countly::setSalt method void (const std::string &) public +cly::Countly::setSha256 method void (cly::SHA256Function) public +cly::Countly::setUpdateInterval method void (size_t) public +cly::Countly::setUserDetails method void (const std::map &) public +cly::Countly::shutdownNetworking method void () static public +cly::Countly::start method void (const std::string &, const std::string &, int, bool) public +cly::Countly::startOnCloud method void (const std::string &) public +cly::Countly::stop method void () public +cly::Countly::updateRemoteConfig method void () public +cly::Countly::updateRemoteConfigExcept method void (std::string *, size_t) public +cly::Countly::updateRemoteConfigFor method void (std::string *, size_t) public +cly::Countly::updateSession method bool () public +cly::Countly::views method cly::ViewsModule &() const public +cly::Countly::~Countly destructor void () virtual public +cly::CountlyConfiguration struct public +cly::CountlyConfiguration::CountlyConfiguration constructor void (const std::string, std::string) public +cly::CountlyConfiguration::appKey field std::string public +cly::CountlyConfiguration::autoEventsOnUserProperties field bool public +cly::CountlyConfiguration::backoffMechanismEnabled field bool public +cly::CountlyConfiguration::breadcrumbsThreshold field unsigned int public +cly::CountlyConfiguration::clearStoredDeviceID field bool public +cly::CountlyConfiguration::crashFilter field CrashFilterCallback public +cly::CountlyConfiguration::crashSegmentation field std::map public +cly::CountlyConfiguration::databasePath field std::string public +cly::CountlyConfiguration::deviceId field std::string public +cly::CountlyConfiguration::deviceIdType field DeviceIdType public +cly::CountlyConfiguration::enrollABOnRCDownload field bool public +cly::CountlyConfiguration::eventQueueThreshold field int public +cly::CountlyConfiguration::failedRequestCooldownSeconds field unsigned int public +cly::CountlyConfiguration::forcePost field bool public +cly::CountlyConfiguration::gradualRequestCleanerEnabled field bool public +cly::CountlyConfiguration::healthCheckEnabled field bool public +cly::CountlyConfiguration::http_client_function field HTTPClientFunction public +cly::CountlyConfiguration::immediateRequestOnStop field bool public +cly::CountlyConfiguration::manualSessionControl field bool public +cly::CountlyConfiguration::maxKeyLength field unsigned int public +cly::CountlyConfiguration::maxProcessingBatchSize field unsigned int public +cly::CountlyConfiguration::maxSegmentationValues field unsigned int public +cly::CountlyConfiguration::maxStackTraceLineLength field unsigned int public +cly::CountlyConfiguration::maxStackTraceLinesPerThread field unsigned int public +cly::CountlyConfiguration::maxValueSize field unsigned int public +cly::CountlyConfiguration::metrics field nlohmann::json public +cly::CountlyConfiguration::port field unsigned int public +cly::CountlyConfiguration::requestDropAgeHours field unsigned int public +cly::CountlyConfiguration::requestQueueThreshold field unsigned int public +cly::CountlyConfiguration::salt field std::string public +cly::CountlyConfiguration::sdkBehaviorSettings field std::string public +cly::CountlyConfiguration::sdkBehaviorSettingsUpdatesDisabled field bool public +cly::CountlyConfiguration::serverPath field std::string public +cly::CountlyConfiguration::serverUrl field std::string public +cly::CountlyConfiguration::sessionDuration field unsigned int public +cly::CountlyConfiguration::sha256_function field SHA256Function public +cly::CountlyDelegates class public +cly::CountlyDelegates::AutomaticSessionTrackingChanged method void () virtual public +cly::CountlyDelegates::ConsentRequirementChanged method void () virtual public +cly::CountlyDelegates::CurrentViewId method std::string () virtual public +cly::CountlyDelegates::DeviceMetrics method nlohmann::json () virtual public +cly::CountlyDelegates::IsSessionRunning method bool () virtual public +cly::CountlyDelegates::LocationSettingChanged method void () virtual public +cly::CountlyDelegates::PlatformName method std::string () virtual public +cly::CountlyDelegates::RecordEvent method void (const std::string &, const std::map &, int) virtual pure public +cly::CountlyDelegates::RecordEvent method void (const std::string &, const std::map &, int, double) virtual pure public +cly::CountlyDelegates::RecordEvent method void (const std::string &, const std::map &, int, double, double) virtual pure public +cly::CountlyDelegates::RecordEvent method void (const std::string &, int) virtual pure public +cly::CountlyDelegates::RecordEvent method void (const std::string &, int, double) virtual pure public +cly::CountlyDelegates::RecordLocation method void (const std::string &, const std::string &, const std::string &, const std::string &) virtual pure public +cly::CountlyDelegates::addEvent method void (const cly::Event &) virtual pure public +cly::CrashData struct public +cly::CrashData::breadcrumbs field std::vector public +cly::CrashData::crashMetrics field std::map public +cly::CrashData::crashSegmentation field std::map public +cly::CrashData::description field std::string public +cly::CrashData::fatal field bool public +cly::CrashData::stackTrace field std::string public +cly::CrashFilterCallback alias std::function public +cly::CrashModule class public +cly::CrashModule::CrashModule constructor void (std::shared_ptr, std::shared_ptr, std::shared_ptr, std::shared_ptr) public +cly::CrashModule::addBreadcrumb method void (const std::string &) public +cly::CrashModule::clearBreadcrumbs method void () public +cly::CrashModule::recordException method void (const std::string &, const std::string &, const bool, const std::map &, const std::map &) public +cly::CrashModule::~CrashModule destructor void () public +cly::DataEntry class public +cly::DataEntry::DataEntry constructor void (const long long, const std::string &) public +cly::DataEntry::getData method const std::string &() const public +cly::DataEntry::getId method long long () const public +cly::DataEntry::~DataEntry destructor void () noexcept public +cly::DeviceIdType enum public +cly::DeviceIdType::DEVELOPER_SUPPLIED enumerator public +cly::DeviceIdType::SDK_GENERATED enumerator public +cly::Event class public +cly::Event::Event constructor void (const std::string &, size_t) public +cly::Event::Event constructor void (const std::string &, size_t, double) public +cly::Event::Event constructor void (const std::string &, size_t, double, double) public +cly::Event::addSegmentation function template void (const std::string &, T) public +cly::Event::applyLimits method void (unsigned int, unsigned int, unsigned int) public +cly::Event::clearSegmentation method void () public +cly::Event::getKey method std::string () const public +cly::Event::hasSegmentation method bool () const public +cly::Event::removeSegmentation method void (const std::string &) public +cly::Event::serialize method std::string () const public +cly::Event::setTimestamp method void () public +cly::Event::startTimer method void () public +cly::Event::stopTimer method void () public +cly::FilterList struct public +cly::FilterList::filterList field T public +cly::FilterList::isWhitelist field bool public +cly::HTTPClientFunction alias std::function public +cly::HTTPResponse struct public +cly::HTTPResponse::body field std::string public +cly::HTTPResponse::data field nlohmann::json public +cly::HTTPResponse::status_code field long public +cly::HTTPResponse::success field bool public +cly::HealthCheckModule class public +cly::HealthCheckModule::COUNT_CAP variable const int static public +cly::HealthCheckModule::HealthCheckModule constructor void () public +cly::HealthCheckModule::MAX_ERROR_MESSAGE_LENGTH variable const size_t static public +cly::HealthCheckModule::clearAndSave method void () public +cly::HealthCheckModule::clearSentValues method void (const nlohmann::json &) public +cly::HealthCheckModule::logBackoffRequest method void () public +cly::HealthCheckModule::logConsecutiveBackoffRequest method void () public +cly::HealthCheckModule::logError method void () public +cly::HealthCheckModule::logFailedNetworkRequest method void (long, const std::string &) public +cly::HealthCheckModule::logWarning method void () public +cly::HealthCheckModule::recordLogCode method void (const std::string &, int) public +cly::HealthCheckModule::recordUsage method void (const std::string &, const std::string &) public +cly::HealthCheckModule::saveState method void () public +cly::HealthCheckModule::setLogger method void (std::weak_ptr) public +cly::HealthCheckModule::setStorage method void (std::shared_ptr) public +cly::HealthCheckModule::snapshot method nlohmann::json () public +cly::HealthCheckModule::~HealthCheckModule destructor void () public +cly::LogLevel enum public +cly::LogLevel::DEBUG enumerator public +cly::LogLevel::ERROR enumerator public +cly::LogLevel::FATAL enumerator public +cly::LogLevel::INFO enumerator public +cly::LogLevel::WARNING enumerator public +cly::LoggerFunction alias std::function public +cly::LoggerModule class public +cly::LoggerModule::LoggerModule constructor void () public +cly::LoggerModule::getLogger method const LoggerFunction () public +cly::LoggerModule::healthCheck method std::shared_ptr () const public +cly::LoggerModule::isInsideCallback method bool () static public +cly::LoggerModule::log method void (LogLevel, const std::string &) public +cly::LoggerModule::recordLogCode method void (const std::string &, int) public +cly::LoggerModule::recordUsage method void (const std::string &, const std::string &) public +cly::LoggerModule::setHealthCheck method void (std::shared_ptr) public +cly::LoggerModule::setLogger method void (LoggerFunction) public +cly::LoggerModule::~LoggerModule destructor void () public +cly::RemoteConfigStore struct public +cly::RemoteConfigStore::app_key field std::string public +cly::RemoteConfigStore::fetch_running field std::atomic public +cly::RemoteConfigStore::has_pending field bool public +cly::RemoteConfigStore::mutex field std::mutex public +cly::RemoteConfigStore::pending_data field std::map public +cly::RemoteConfigStore::pending_merge field bool public +cly::RemoteConfigStore::user_generation field unsigned long long public +cly::RemoteConfigStore::values field nlohmann::json public +cly::RequestBuilder class public +cly::RequestBuilder::RequestBuilder constructor void (std::shared_ptr, std::shared_ptr) public +cly::RequestBuilder::buildFetchRequest method std::string (const std::map &) public +cly::RequestBuilder::buildRequest method std::string (const std::map &) public +cly::RequestBuilder::encodeURL method std::string (const std::string &) static public +cly::RequestBuilder::serializeData method std::string (const std::map &) static public +cly::RequestBuilder::~RequestBuilder destructor void () public +cly::RequestModule class public +cly::RequestModule::RQSize method long long () public +cly::RequestModule::RequestModule constructor void (std::shared_ptr, std::shared_ptr, std::shared_ptr, std::shared_ptr) public +cly::RequestModule::addRequestToQueue method void (const std::map &) public +cly::RequestModule::clearRequestQueue method void () public +cly::RequestModule::initGlobalNetworking method void () static public +cly::RequestModule::processQueue method void (std::shared_ptr, bool) public +cly::RequestModule::removeOtherAppKeysFromQueue method long long (const std::string &) public +cly::RequestModule::replaceAppKeysInQueue method void (const std::string &) public +cly::RequestModule::sendHTTP method HTTPResponse (std::string, std::string) public +cly::RequestModule::sendImmediate method HTTPResponse (const std::string &, const std::string &) public +cly::RequestModule::setConfigurationProvider method void (std::weak_ptr) public +cly::RequestModule::setOnMergeDelivered method void (std::function) public +cly::RequestModule::setServerUrl method void (const std::string &, unsigned int, const std::string &) public +cly::RequestModule::~RequestModule destructor void () public +cly::SDKLimits struct public +cly::SDKLimits::maxBreadcrumbCount field unsigned int public +cly::SDKLimits::maxKeyLength field unsigned int public +cly::SDKLimits::maxSegmentationValues field unsigned int public +cly::SDKLimits::maxStackTraceLineLength field unsigned int public +cly::SDKLimits::maxStackTraceLinesPerThread field unsigned int public +cly::SDKLimits::maxValueSize field unsigned int public +cly::SHA256Function alias std::function public +cly::StorageModuleBase class public +cly::StorageModuleBase::RQClearAll method void () virtual pure public +cly::StorageModuleBase::RQCount method long long () virtual pure public +cly::StorageModuleBase::RQInsertAtEnd method void (const std::string &) virtual pure public +cly::StorageModuleBase::RQPeekAll method std::vector> () virtual pure public +cly::StorageModuleBase::RQPeekFront method const std::shared_ptr () virtual pure public +cly::StorageModuleBase::RQRemove method void (long long) virtual public +cly::StorageModuleBase::RQRemoveFront method void () virtual pure public +cly::StorageModuleBase::RQRemoveFront method void (std::shared_ptr) virtual pure public +cly::StorageModuleBase::RQUpdate method void (long long, const std::string &) virtual public +cly::StorageModuleBase::StorageModuleBase constructor void (std::shared_ptr, std::shared_ptr) public +cly::StorageModuleBase::_configuration field std::shared_ptr protected +cly::StorageModuleBase::_is_initialized field bool protected +cly::StorageModuleBase::_logger field std::shared_ptr protected +cly::StorageModuleBase::getDeviceIdState method std::string () virtual public +cly::StorageModuleBase::getHealthCheckState method std::string () virtual public +cly::StorageModuleBase::getSDKBehaviorSettings method std::string () virtual pure public +cly::StorageModuleBase::init method void () virtual pure public +cly::StorageModuleBase::isInitialized method bool () public +cly::StorageModuleBase::storeDeviceIdState method void (const std::string &) virtual public +cly::StorageModuleBase::storeHealthCheckState method void (const std::string &) virtual public +cly::StorageModuleBase::storeSDKBehaviorSettings method void (const std::string &) virtual pure public +cly::StorageModuleBase::~StorageModuleBase destructor void () noexcept virtual public +cly::ViewsModule class public +cly::ViewsModule::ViewsModule constructor void (cly::CountlyDelegates *, std::shared_ptr) public +cly::ViewsModule::closeViewWithID method void (const std::string &, const std::map &) public +cly::ViewsModule::closeViewWithName method void (const std::string &, const std::map &) public +cly::ViewsModule::openView method std::string (const std::string &, const std::map &) public +cly::ViewsModule::stopAllViews method void (const std::map &) public +cly::ViewsModule::~ViewsModule destructor void () public +cly::consent::CRASHES variable const char *const constexpr public +cly::consent::EVENTS variable const char *const constexpr public +cly::consent::FEEDBACK variable const char *const constexpr public +cly::consent::LOCATION variable const char *const constexpr public +cly::consent::METRICS variable const char *const constexpr public +cly::consent::REMOTE_CONFIG variable const char *const constexpr public +cly::consent::SESSIONS variable const char *const constexpr public +cly::consent::STAR_RATING variable const char *const constexpr public +cly::consent::USERS variable const char *const constexpr public +cly::consent::VIEWS variable const char *const constexpr public +cly::consent::allFeatures function std::vector () public +cly::utils::format_string function std::string (const std::string &) static public +cly::utils::format_string function template std::string (const std::string &, Args...) static public +cly::utils::generateEventID function std::string () public +cly::utils::generateUUID function std::string () public +cly::utils::gmTime function std::tm (std::time_t) public +cly::utils::localTime function std::tm (std::time_t) public +cly::utils::mapToString function std::string (const std::map &) static public diff --git a/api/requests/consent.json b/api/requests/consent.json new file mode 100644 index 0000000..b2b71f9 --- /dev/null +++ b/api/requests/consent.json @@ -0,0 +1,193 @@ +[ + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "hc": { + "bom": "", + "cbom": "", + "el": "", + "em": "", + "fu": "", + "lc": "", + "sc": "", + "wl": "" + }, + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "consent": { + "crashes": false, + "events": false, + "feedback": false, + "location": false, + "metrics": false, + "remote-config": false, + "sessions": false, + "star-rating": false, + "users": false, + "views": false + }, + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "location": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "consent": { + "crashes": false, + "events": true, + "feedback": false, + "location": false, + "metrics": false, + "remote-config": false, + "sessions": true, + "star-rating": false, + "users": false, + "views": false + }, + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "begin_session": "1", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "location": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "events": [ + { + "count": 1, + "cvid": "", + "dow": "", + "hour": "", + "id": "", + "key": "consented", + "peid": "", + "timestamp": "" + } + ], + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "consent": { + "crashes": false, + "events": false, + "feedback": false, + "location": false, + "metrics": false, + "remote-config": false, + "sessions": true, + "star-rating": false, + "users": false, + "views": false + }, + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "end_session": "1", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "session_duration": "", + "t": "0", + "timestamp": "", + "tz": "" + } + } +] diff --git a/api/requests/crash.json b/api/requests/crash.json new file mode 100644 index 0000000..3b15790 --- /dev/null +++ b/api/requests/crash.json @@ -0,0 +1,62 @@ +[ + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "hc": { + "bom": "", + "cbom": "", + "el": "", + "em": "", + "fu": "", + "lc": "", + "sc": "", + "wl": "" + }, + "hour": "", + "metrics": { + "_app_version": "" + }, + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "crash": { + "_app_version": "1.0", + "_custom": { + "tier": "gold" + }, + "_device": "", + "_error": "", + "_logs": "breadcrumb one\n", + "_name": "parity crash", + "_nonfatal": true, + "_ob": 0, + "_os": "", + "_os_version": "", + "_resolution": "" + }, + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + } +] diff --git a/api/requests/device-id.json b/api/requests/device-id.json new file mode 100644 index 0000000..8124311 --- /dev/null +++ b/api/requests/device-id.json @@ -0,0 +1,147 @@ +[ + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "hc": { + "bom": "", + "cbom": "", + "el": "", + "em": "", + "fu": "", + "lc": "", + "sc": "", + "wl": "" + }, + "hour": "", + "metrics": { + "_app_version": "" + }, + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "begin_session": "1", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "metrics": { + "_app_version": "", + "_carrier": "", + "_device": "", + "_os": "", + "_os_version": "", + "_resolution": "" + }, + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-002", + "dow": "", + "hour": "", + "old_device_id": "parity-device-001", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-002", + "dow": "", + "events": [ + { + "count": 1, + "cvid": "", + "dow": "", + "hour": "", + "id": "", + "key": "after_merge", + "peid": "", + "timestamp": "" + } + ], + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-003", + "dow": "", + "events": [ + { + "count": 1, + "cvid": "", + "dow": "", + "hour": "", + "id": "", + "key": "after_new_id", + "peid": "", + "timestamp": "" + } + ], + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-003", + "dow": "", + "end_session": "1", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "session_duration": "", + "t": "0", + "timestamp": "", + "tz": "" + } + } +] diff --git a/api/requests/parity.json b/api/requests/parity.json new file mode 100644 index 0000000..91945d0 --- /dev/null +++ b/api/requests/parity.json @@ -0,0 +1,221 @@ +[ + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "hc": { + "bom": "", + "cbom": "", + "el": "", + "em": "", + "fu": "", + "lc": "", + "sc": "", + "wl": "" + }, + "hour": "", + "metrics": { + "_app_version": "" + }, + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "begin_session": "1", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "metrics": { + "_app_version": "", + "_carrier": "", + "_device": "", + "_os": "", + "_os_version": "", + "_resolution": "" + }, + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "events": [ + { + "count": 2, + "cvid": "", + "dow": "", + "dur": 5.0, + "hour": "", + "id": "", + "key": "purchase", + "peid": "", + "segmentation": { + "boolList": [ + true, + false, + true + ], + "doubleList": [ + 1.1, + 2.2, + 3.3 + ], + "hashirama": -2147483647, + "intList": [ + 1, + 2, + 3 + ], + "itachi": 1.7976931348623157e+308, + "kakaski": 5e-324, + "mapList": [ + { + "key1": "value1", + "key2": 2 + }, + { + "key1": "value2", + "key2": 3 + }, + { + "key1": "value3", + "key2": 4 + } + ], + "mixedList": [ + "value1", + 2, + 3.3, + true + ], + "nar": "uto", + "sakura": false, + "sasuke": 2147483647, + "stringList": [ + "value1", + "value2", + "value3" + ], + "tobi": { + "obito": "uchiha", + "zetsu": "white" + } + }, + "sum": 9.99, + "timestamp": "" + } + ], + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "", + "user_details": { + "byear": "1980", + "email": "john@example.com", + "gender": "M", + "name": "John Doe", + "organization": "Countly", + "phone": "1234567890", + "picture": "https://example.com/picture.jpg", + "username": "john_d" + } + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "", + "user_details": { + "custom": { + "age": "30", + "tier": "gold" + } + } + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "session_duration": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "end_session": "1", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "session_duration": "", + "t": "0", + "timestamp": "", + "tz": "" + } + } +] diff --git a/api/requests/views.json b/api/requests/views.json new file mode 100644 index 0000000..8353f78 --- /dev/null +++ b/api/requests/views.json @@ -0,0 +1,144 @@ +[ + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "hc": { + "bom": "", + "cbom": "", + "el": "", + "em": "", + "fu": "", + "lc": "", + "sc": "", + "wl": "" + }, + "hour": "", + "metrics": { + "_app_version": "" + }, + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "begin_session": "1", + "device_id": "parity-device-001", + "dow": "", + "hour": "", + "metrics": { + "_app_version": "", + "_carrier": "", + "_device": "", + "_os": "", + "_os_version": "", + "_resolution": "" + }, + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "events": [ + { + "count": 1, + "dow": "", + "dur": "", + "hour": "", + "id": "", + "key": "[CLY]_view", + "pvid": "", + "segmentation": { + "_idv": "", + "name": "Home", + "segment": "parity-os", + "source": "scenario", + "start": "1", + "visit": "1" + }, + "sum": 0.0, + "timestamp": "" + }, + { + "count": 1, + "dow": "", + "dur": "", + "hour": "", + "id": "", + "key": "[CLY]_view", + "pvid": "", + "segmentation": { + "_idv": "", + "name": "Home", + "segment": "parity-os" + }, + "sum": 0.0, + "timestamp": "" + }, + { + "count": 1, + "dow": "", + "dur": "", + "hour": "", + "id": "", + "key": "[CLY]_view", + "pvid": "", + "segmentation": { + "_idv": "", + "name": "Settings", + "segment": "parity-os", + "visit": "1" + }, + "sum": 0.0, + "timestamp": "" + } + ], + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "t": "0", + "timestamp": "", + "tz": "" + } + }, + { + "path": "/i", + "params": { + "app_key": "parity-app-key", + "av": "", + "device_id": "parity-device-001", + "dow": "", + "end_session": "1", + "hour": "", + "rr": "", + "sdk_name": "cpp-native-unknown", + "sdk_version": "", + "session_duration": "", + "t": "0", + "timestamp": "", + "tz": "" + } + } +] diff --git a/tests/request_scenarios.cpp b/tests/request_scenarios.cpp new file mode 100644 index 0000000..83922ea --- /dev/null +++ b/tests/request_scenarios.cpp @@ -0,0 +1,324 @@ +// In each request scenario the SDK sends the requests api/requests/.json records, with the values that change +// from run to run masked. The scenarios make the calls the other Countly SDKs make in their own request tests. A +// deliberate change: run the tests with COUNTLY_UPDATE_REQUESTS=1, for example +// COUNTLY_UPDATE_REQUESTS=1 ./build/countly-tests -tc="request scenario*", which writes the records, check the +// difference and add a line to CHANGELOG.md. +#include "countly.hpp" +#include "doctest.h" +#include "nlohmann/json.hpp" +#include "test_utils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace cly; + +namespace { +const char *const MASK = ""; +const std::set MASKED_PARAMS = {"timestamp", "hour", "dow", "tz", "sdk_version", "av", "rr", "checksum", "checksum256", "session_duration"}; +const std::set JSON_PARAMS = {"events", "metrics", "user_details", "crash", "consent", "hc"}; +const std::set MASKED_EVENT_FIELDS = {"timestamp", "hour", "dow", "id", "pvid", "cvid", "peid"}; +const std::set MASKED_CRASH_FIELDS = {"_error", "_run", "_os", "_os_version", "_resolution", "_density", "_locale", "_device", "_device_type", "_manufacturer", "_cpu", "_opengl", "_ram_current", "_ram_total", "_disk_current", "_disk_total", "_bat", "_orientation", "_root", "_online", "_muted", "_background"}; +// a view's "_idv" is the ID the SDK generates for it +const std::set MASKED_VIEW_SEGMENTS = {"_idv"}; +const int MAX_INT = 2147483647; +const std::string RECORDS = std::string(COUNTLY_SOURCE_DIR) + "/api/requests/"; + +std::mutex sent_mutex; +std::vector sent; + +/** + * A copy of an object with the values of the given keys masked. + * + * @param value: object + * @param keys: keys to mask, or nullptr to mask every key + * @return the masked copy + */ +nlohmann::json masked(const nlohmann::json &value, const std::set *keys) { + nlohmann::json copy = nlohmann::json::object(); + for (const auto &item : value.items()) { + copy[item.key()] = (keys == nullptr || keys->count(item.key()) > 0) ? nlohmann::json(MASK) : item.value(); + } + return copy; +} + +/** + * The value of a JSON parameter with the values that change from run to run masked. + * + * @param name: parameter name + * @param value: parsed parameter value + * @return the masked value + */ +nlohmann::json maskedJson(const std::string &name, const nlohmann::json &value) { + if ((name == "metrics" || name == "hc") && value.is_object()) { + return masked(value, nullptr); + } + if (name == "events" && value.is_array()) { + nlohmann::json events = nlohmann::json::array(); + for (const auto &event : value) { + if (!event.is_object()) { + events.push_back(event); + continue; + } + std::set fields = MASKED_EVENT_FIELDS; + const bool view = event.value("key", "") == "[CLY]_view"; + if (view) { + fields.insert("dur"); + } + nlohmann::json maskedEvent = masked(event, &fields); + if (view && maskedEvent.contains("segmentation") && maskedEvent["segmentation"].is_object()) { + maskedEvent["segmentation"] = masked(maskedEvent["segmentation"], &MASKED_VIEW_SEGMENTS); + } + events.push_back(maskedEvent); + } + return events; + } + if (name == "crash" && value.is_object()) { + return masked(value, &MASKED_CRASH_FIELDS); + } + return value; +} + +/** + * Decodes the %XX escapes of a request parameter. + * + * @param encoded: the value as the SDK sent it + * @return the decoded value + */ +std::string decoded(const std::string &encoded) { + std::string value; + for (size_t i = 0; i < encoded.size(); i++) { + if (encoded[i] == '%' && i + 2 < encoded.size()) { + value += static_cast(std::strtol(encoded.substr(i + 1, 2).c_str(), nullptr, 16)); + i += 2; + } else { + value += encoded[i]; + } + } + return value; +} + +/** + * HTTP client of the scenarios: keeps every request as the record writes it and answers it with success. + * + * @param use_post: whether the SDK sends the request as POST + * @param url: path of the request + * @param data: the URL encoded parameters + * @return a successful response + */ +HTTPResponse recordRequest(bool use_post, const std::string &url, const std::string &data) { + (void)use_post; + nlohmann::json params = nlohmann::json::object(); + std::stringstream pairs(data); + std::string pair; + while (std::getline(pairs, pair, '&')) { + const size_t assignment = pair.find('='); + const std::string name = pair.substr(0, assignment); + const std::string value = assignment == std::string::npos ? "" : decoded(pair.substr(assignment + 1)); + if (MASKED_PARAMS.count(name) > 0) { + params[name] = MASK; + } else if (JSON_PARAMS.count(name) > 0) { + nlohmann::json parsed = nlohmann::json::parse(value, nullptr, false); + params[name] = parsed.is_discarded() ? nlohmann::json(value) : maskedJson(name, parsed); + } else { + params[name] = value; + } + } + nlohmann::ordered_json request; + request["path"] = url; + request["params"] = params; + std::lock_guard lock(sent_mutex); + sent.push_back(request); + return HTTPResponse{true, nlohmann::json({{"result", "Success"}}), 200}; +} + +/** + * The number of requests sent so far, or of those that carry the given parameter. + * + * @param param: parameter to look for, or empty to count every request + * @return the count + */ +size_t sentCount(const std::string ¶m = "") { + std::lock_guard lock(sent_mutex); + size_t count = 0; + for (const auto &request : sent) { + if (param.empty() || request["params"].contains(param)) { + count++; + } + } + return count; +} + +/** + * Moves the recorded events into the request queue and waits until the queue is sent, so that what the calls before + * queued stays apart from what follows. + * + * @param countly: the SDK + */ +void settle(Countly &countly) { + countly.flushEvents(); + countly.processRQDebug(); + // processRQDebug returns at once while the update loop is sending the queue + for (int waited = 0; waited < 10000 && countly.checkRQSize() > 0; waited += 10) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } +} + +/** + * Describes where a record and the requests the SDK sent part: the first line that differs. + * + * @param record: content of the record + * @param current: the record of the requests the SDK sent + * @return the line number with both versions of the line, or an empty string when they are the same + */ +std::string firstDifference(const std::string &record, const std::string ¤t) { + std::stringstream recorded(record); + std::stringstream sentNow(current); + std::string recordedLine; + std::string sentLine; + for (int line = 1;; line++) { + const bool moreRecorded = static_cast(std::getline(recorded, recordedLine)); + const bool moreSent = static_cast(std::getline(sentNow, sentLine)); + if (!moreRecorded && !moreSent) { + return ""; + } + if (moreRecorded != moreSent || recordedLine != sentLine) { + return "line " + std::to_string(line) + ": the record has [" + (moreRecorded ? recordedLine : "(end)") + "], the SDK sent [" + (moreSent ? sentLine : "(end)") + "]"; + } + } +} + +/** + * Runs a scenario on a new SDK instance and compares the requests it sent with the record of the scenario, or writes + * the record when COUNTLY_UPDATE_REQUESTS is set. + * + * @param name: scenario name, the record file name without .json + * @param requiresConsent: whether the scenario starts the SDK with consent required + * @param scenario: the calls, given the SDK + */ +void checkRequests(const std::string &name, bool requiresConsent, const std::function &scenario) { + test_utils::clearSDK(); + { + std::lock_guard lock(sent_mutex); + sent.clear(); + } + Countly &countly = Countly::getInstance(); + countly.setHTTPClient(recordRequest); + countly.setDeviceID("parity-device-001"); + countly.SetPath(TEST_DATABASE_NAME); + countly.enableManualSessionControl(); + countly.setRequiresConsent(requiresConsent); + countly.setMetrics("parity-os", "1.0", "parity-device", "1x1", "parity-carrier", "1.0"); + // the settings are fetched on a thread of their own, whose requests would come between the queued ones at random + countly.disableSDKBehaviorSettingsUpdates(); + countly.start("parity-app-key", "https://parity.example", 443, false); + // the update loop sends the health check when it starts + for (int waited = 0; waited < 5000 && sentCount("hc") == 0; waited += 10) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + scenario(countly); + settle(countly); + + std::string current; + { + std::lock_guard lock(sent_mutex); + current = nlohmann::ordered_json(sent).dump(2) + "\n"; + } + test_utils::clearSDK(); + + const std::string file = RECORDS + name + ".json"; + // only whether the variable is set is read, never its value + if (std::getenv("COUNTLY_UPDATE_REQUESTS") != nullptr) { // Flawfinder: ignore + std::ofstream out(file, std::ios::binary); + out << current; + REQUIRE(out.good()); + return; + } + std::ifstream in(file, std::ios::binary); + std::stringstream committed; + committed << in.rdbuf(); + std::string record = committed.str(); + for (size_t at = record.find("\r\n"); at != std::string::npos; at = record.find("\r\n", at)) { + record.erase(at, 1); + } + const std::string difference = firstDifference(record, current); + INFO("the requests of the " << name << " scenario differ from api/requests/" << name << ".json. If that is deliberate, run the tests with COUNTLY_UPDATE_REQUESTS=1, check the difference and add a line to CHANGELOG.md."); + CHECK_MESSAGE(difference.empty(), difference); +} +} // namespace + +TEST_CASE("request scenario: parity") { + checkRequests("parity", false, [](Countly &countly) { + countly.beginSession(); + Event event("purchase", 2, 9.99, 5); + event.addSegmentation("nar", "uto"); + event.addSegmentation("sasuke", MAX_INT); + event.addSegmentation("sakura", false); + event.addSegmentation("itachi", std::numeric_limits::max()); + event.addSegmentation("kakaski", std::numeric_limits::denorm_min()); + event.addSegmentation("hashirama", -MAX_INT); + event.addSegmentation("tobi", nlohmann::json({{"obito", "uchiha"}, {"zetsu", "white"}})); + event.addSegmentation("stringList", std::vector{"value1", "value2", "value3"}); + event.addSegmentation("intList", std::vector{1, 2, 3}); + event.addSegmentation("doubleList", std::vector{1.1, 2.2, 3.3}); + event.addSegmentation("boolList", std::vector{true, false, true}); + event.addSegmentation("mixedList", nlohmann::json::array({"value1", 2, 3.3, true})); + event.addSegmentation("mapList", nlohmann::json::array({nlohmann::json({{"key1", "value1"}, {"key2", 2}}), nlohmann::json({{"key1", "value2"}, {"key2", 3}}), nlohmann::json({{"key1", "value3"}, {"key2", 4}})})); + countly.addEvent(event); + countly.setUserDetails({{"name", "John Doe"}, {"username", "john_d"}, {"email", "john@example.com"}, {"organization", "Countly"}, {"phone", "1234567890"}, {"picture", "https://example.com/picture.jpg"}, {"gender", "M"}, {"byear", "1980"}}); + countly.setCustomUserDetails({{"tier", "gold"}, {"age", "30"}}); + // a session update reports elapsed seconds, so at least one has to pass for it to be sent + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + countly.updateSession(); + countly.endSession(); + }); +} + +TEST_CASE("request scenario: views") { + checkRequests("views", false, [](Countly &countly) { + countly.beginSession(); + countly.views().openView("Home", {{"source", "scenario"}}); + countly.views().closeViewWithName("Home"); + countly.views().openView("Settings"); + countly.endSession(); + }); +} + +TEST_CASE("request scenario: crash") { + checkRequests("crash", false, [](Countly &countly) { + countly.crash().addBreadcrumb("breadcrumb one"); + countly.crash().recordException("parity crash", "parity crash\n at the scenario", false, {{"_os", "parity-os"}, {"_app_version", "1.0"}}, {{"tier", "gold"}}); + }); +} + +TEST_CASE("request scenario: consent") { + checkRequests("consent", true, [](Countly &countly) { + countly.giveConsent({"sessions", "events"}); + countly.beginSession(); + countly.RecordEvent("consented", 1); + settle(countly); + countly.removeConsent({"events"}); + countly.RecordEvent("not_consented", 1); + countly.endSession(); + }); +} + +TEST_CASE("request scenario: device-id") { + checkRequests("device-id", false, [](Countly &countly) { + countly.beginSession(); + countly.setDeviceID("parity-device-002", true); + countly.RecordEvent("after_merge", 1); + settle(countly); + countly.setDeviceID("parity-device-003", false); + countly.RecordEvent("after_new_id", 1); + countly.endSession(); + }); +}