diff --git a/examples/qt_demo/.gitignore b/examples/qt_demo/.gitignore
new file mode 100644
index 0000000..a2919a1
--- /dev/null
+++ b/examples/qt_demo/.gitignore
@@ -0,0 +1,32 @@
+# Build directory
+build/
+
+# CMake generated files
+CMakeCache.txt
+CMakeFiles/
+cmake_install.cmake
+Makefile
+
+# Qt auto-generated
+*_autogen/
+moc_*
+ui_*
+qrc_*
+
+# Compiled binaries
+*.o
+*.obj
+*.exe
+*.out
+*.app
+
+# IDE files
+.vscode/
+.idea/
+*.user
+*.swp
+*~
+.DS_Store
+
+# Dev credentials — do not commit
+dev_config.hpp
diff --git a/examples/qt_demo/CMakeLists.txt b/examples/qt_demo/CMakeLists.txt
new file mode 100644
index 0000000..fb11344
--- /dev/null
+++ b/examples/qt_demo/CMakeLists.txt
@@ -0,0 +1,29 @@
+cmake_minimum_required(VERSION 3.16)
+project(qt_demo)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_AUTOMOC ON)
+
+find_package(CURL REQUIRED)
+find_package(Qt6 REQUIRED COMPONENTS Widgets WebEngineWidgets)
+
+# Countly SDK paths — defaults to the parent SDK checkout (this demo lives in
+# countly-sdk-cpp/examples/qt_demo/). Override with -DCOUNTLY_SDK_DIR=... if
+# you keep the SDK somewhere else.
+if(NOT COUNTLY_SDK_DIR)
+ get_filename_component(COUNTLY_SDK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
+endif()
+set(COUNTLY_BUILD_DIR "${COUNTLY_SDK_DIR}/build")
+
+add_executable(qt_demo main.cpp)
+target_compile_definitions(qt_demo PRIVATE COUNTLY_USE_SQLITE)
+target_include_directories(qt_demo PRIVATE
+ ${COUNTLY_SDK_DIR}/include
+ ${COUNTLY_SDK_DIR}/vendor/json/include
+)
+target_link_libraries(qt_demo PRIVATE
+ CURL::libcurl
+ Qt6::Widgets
+ Qt6::WebEngineWidgets
+ ${COUNTLY_BUILD_DIR}/libcountly.dylib
+)
diff --git a/examples/qt_demo/Countly_Feedback_Widget_Implementation_Guide.html b/examples/qt_demo/Countly_Feedback_Widget_Implementation_Guide.html
new file mode 100644
index 0000000..039d5e7
--- /dev/null
+++ b/examples/qt_demo/Countly_Feedback_Widget_Implementation_Guide.html
@@ -0,0 +1,512 @@
+
+
+
+
+Countly Feedback Widgets - C++ Implementation Guide
+
+
+
+
+
+
+ This guide covers how to implement Countly Feedback Widgets (NPS, Survey, Rating) in a C++ application.
+ There are two approaches: WebView Presentation (server renders the UI) and Manual Reporting (you build the UI, report results as events).
+
+
+ For product details on each widget type, see:
+
+
+
+ For SDK implementation specs, see:
+
+
+
+
+
+Data Structures
+
+struct CountlyFeedbackWidget {
+ std::string widgetId; // from "_id"
+ std::string type; // "nps", "survey", or "rating"
+ std::string name; // display name
+ std::vector<std::string> tags; // from "tg" array
+ std::string widgetVersion; // from "wv" (empty = legacy)
+};
+
+
+ Widget Version (wv): When present, the widget uses fullscreen display
+ with a webview-driven close button. When absent, it's a legacy widget - you provide the close button.
+
+
+
+
+Step 1: Fetch Available Widgets
+
+Endpoint
+GET /o/sdk?method=feedback
+ &app_key=APP_KEY
+ &device_id=DEVICE_ID
+ &sdk_version=SDK_VERSION
+ &sdk_name=SDK_NAME
+
+
+ If parameter tampering is enabled, add the sha256 checksum parameter.
+ In temporary device ID mode, return an empty list instead.
+
+
+Response
+{
+ "result": [
+ {
+ "_id": "614871419f030e44be07d82f",
+ "type": "rating",
+ "name": "Leave us a feedback",
+ "tg": ["/"]
+ },
+ {
+ "_id": "614871419f030e44be07d839",
+ "type": "nps",
+ "name": "One response for all",
+ "tg": [],
+ "wv": "a"
+ }
+ ]
+}
+
+C++ Example
+// libcurl + nlohmann/json
+std::vector<CountlyFeedbackWidget> fetchWidgets() {
+ std::string url = SERVER_URL + "/o/sdk"
+ "?method=feedback"
+ "&app_key=" + urlEncode(APP_KEY) +
+ "&device_id=" + urlEncode(DEVICE_ID) +
+ "&sdk_version=" + urlEncode(SDK_VERSION) +
+ "&sdk_name=" + urlEncode(SDK_NAME);
+
+ std::string response = httpGet(url);
+ json j = json::parse(response);
+ std::vector<CountlyFeedbackWidget> widgets;
+
+ for (const auto& w : j["result"]) {
+ CountlyFeedbackWidget widget;
+ widget.widgetId = w.value("_id", "");
+ widget.type = w.value("type", "");
+ widget.name = w.value("name", "");
+ widget.widgetVersion = w.value("wv", "");
+
+ if (w.contains("tg") && w["tg"].is_array()) {
+ for (const auto& tag : w["tg"])
+ widget.tags.push_back(tag.get<std::string>());
+ }
+ widgets.push_back(widget);
+ }
+ return widgets;
+}
+
+
+
+Step 2a: WebView Presentation
+
+URL Construction
+/feedback/{type}?widget_id=WIDGET_ID
+ &device_id=DEVICE_ID
+ &app_key=APP_KEY
+ &sdk_version=SDK_VERSION
+ &sdk_name=SDK_NAME
+ &app_version=APP_VERSION
+ &platform=PLATFORM
+ &custom=CUSTOM_JSON
+
+
+ Even if parameter tamper protection is enabled, this URL does not use the checksum parameter.
+
+
+Custom Parameters
+The custom field is a URL-encoded JSON object:
+
+
+ | Flag | Value | Description | When |
+ tc | 1 | Terms & Conditions link support | Always |
+ xb | 1 | WebView handles its own close button | wv present |
+ rw | 1 | Fullscreen display | wv present |
+
+
+
+ - Legacy widget (no
wv): {"tc":1}
+ - Versioned widget (has
wv): {"tc":1, "xb":1, "rw":1}
+
+
+Display Modes
+
+ - Legacy (no
wv): Show in a dialog/window. You provide the close button.
+ - Versioned (has
wv): Fullscreen/transparent overlay. The WebView renders its own close button and communicates via the protocol below.
+
+
+C++ Example
+std::string constructWebViewUrl(const CountlyFeedbackWidget& widget) {
+ json custom;
+ custom["tc"] = 1;
+ if (!widget.widgetVersion.empty()) {
+ custom["xb"] = 1;
+ custom["rw"] = 1;
+ }
+
+ return SERVER_URL + "/feedback/" + widget.type
+ + "?widget_id=" + urlEncode(widget.widgetId)
+ + "&device_id=" + urlEncode(DEVICE_ID)
+ + "&app_key=" + urlEncode(APP_KEY)
+ + "&sdk_version=" + urlEncode(SDK_VERSION)
+ + "&sdk_name=" + urlEncode(SDK_NAME)
+ + "&app_version=" + urlEncode(APP_VERSION)
+ + "&platform=" + urlEncode(PLATFORM)
+ + "&custom=" + urlEncode(custom.dump());
+}
+
+
+
+WebView Communication Protocol
+
+
+ The widget page communicates with your app through URL interception (not a JS bridge).
+ The widget triggers navigation to special URLs that your WebView intercepts.
+
+Base communication URL:
+https://countly_action_event
+Intercept any URL starting with this prefix, URL-decode it, parse the query params, and handle accordingly.
+
+Widget Command (Close)
+Triggered when the widget's built-in close button is pressed (versioned widgets with xb=1):
+https://countly_action_event/?cly_widget_command=1&close=1
+Your app should:
+
+ - Close/dismiss the WebView
+ - Record a cancel event with
"closed":"1" (see Manual Reporting)
+ - Notify any developer callback
+
+
+Action Events
+Link navigation from within the widget:
+https://countly_action_event/?cly_x_action_event=1&action=link&link=URL
+Open the URL in the system browser. If close=1 is also present, dismiss the WebView after.
+
+External Link Interception
+
+ Any URL with cly_x_int=1 as a query parameter should be opened in the system browser.
+ This is used for Terms & Conditions and Privacy Policy links.
+
+https://example.com/terms?cly_x_int=1
+
+ These links often use target="_blank", so your WebView must also handle
+ new window requests (e.g. Qt's createWindow()).
+
+
+Page Load Handling
+
+ - Start the WebView as invisible and non-interactive
+ - After full page load, make it visible and interactive
+ - If loading takes 60 seconds or more, treat as failure and close
+ - Critical resource errors (
js, css, png, jpg, jpeg, webp) or SSL errors should also trigger failure
+
+
+C++ Example - URL Interception (Qt)
+class CountlyWebPage : public QWebEnginePage {
+public:
+ // Handle target="_blank" links (T&C, privacy policy)
+ QWebEnginePage* createWindow(WebWindowType) override {
+ auto* tempPage = new QWebEnginePage(this->profile(), this);
+ connect(tempPage, &QWebEnginePage::urlChanged,
+ [tempPage](const QUrl& url) {
+ QDesktopServices::openUrl(url);
+ tempPage->deleteLater();
+ });
+ return tempPage;
+ }
+
+protected:
+ bool acceptNavigationRequest(
+ const QUrl& url, NavigationType, bool) override
+ {
+ QString urlStr = QUrl::fromPercentEncoding(url.toEncoded());
+
+ // External link: cly_x_int=1
+ QUrlQuery query(url);
+ if (query.hasQueryItem("cly_x_int")
+ && query.queryItemValue("cly_x_int") == "1") {
+ QDesktopServices::openUrl(url);
+ return false;
+ }
+
+ // Communication URL
+ if (urlStr.startsWith("https://countly_action_event")) {
+ QUrlQuery q{QUrl{urlStr}};
+
+ // Widget close command
+ if (q.hasQueryItem("cly_widget_command")) {
+ // dismiss WebView, record cancel event
+ return false;
+ }
+
+ // Action: link
+ if (q.hasQueryItem("cly_x_action_event")) {
+ QString action = q.queryItemValue("action");
+ if (action == "link")
+ QDesktopServices::openUrl(
+ QUrl(q.queryItemValue("link")));
+ return false;
+ }
+ return false;
+ }
+
+ return true; // allow normal navigation
+ }
+};
+
+
+
+Step 2b: Manual Reporting
+
+Build your own UI. The flow: fetch widgets (Step 1) → fetch widget data → report results as events.
+
+Fetch Widget Data
+GET /o/surveys/{type}/widget
+ ?widget_id=WIDGET_ID
+ &shown=1
+ &sdk_version=SDK_VERSION
+ &sdk_name=SDK_NAME
+ &app_version=APP_VERSION
+ &platform=PLATFORM
+This is a direct request (not through the event/request queue).
+
+C++ Example
+json getFeedbackWidgetData(const CountlyFeedbackWidget& widget) {
+ std::string url = SERVER_URL
+ + "/o/surveys/" + widget.type + "/widget"
+ + "?widget_id=" + urlEncode(widget.widgetId)
+ + "&shown=1"
+ + "&sdk_version=" + urlEncode(SDK_VERSION)
+ + "&sdk_name=" + urlEncode(SDK_NAME)
+ + "&app_version=" + urlEncode(APP_VERSION)
+ + "&platform=" + urlEncode(PLATFORM);
+
+ std::string response = httpGet(url);
+ return json::parse(response);
+}
+
+Reporting Results
+Report as an event. The event key depends on widget type:
+
+
+ | Widget Type | Event Key |
+ | NPS | [CLY]_nps |
+ | Survey | [CLY]_survey |
+ | Rating | [CLY]_star_rating |
+
+
+Segmentation
+Every widget event includes this base segmentation:
+
+ | Key | Value |
+ platform | SDK platform identifier |
+ app_version | Host application version |
+ widget_id | The widget's ID |
+
+The widgetResult object (user's answers) is merged into this segmentation.
+For the full widgetResult structure per widget type, see
+A Deeper Look - Widget Strucures.
+
+Closed Widget
+If the user closes without completing, report with "closed":"1":
+{
+ "platform": "desktop",
+ "app_version": "1.0.0",
+ "widget_id": "614871419f030e44be07d82f",
+ "closed": "1"
+}
+
+
+ After recording the event, force flush immediately - don't wait for the event count threshold.
+
+
+C++ Example
+void reportFeedbackWidget(
+ const CountlyFeedbackWidget& widget,
+ const json& widgetResult) // null json if closed
+{
+ std::string key;
+ if (widget.type == "nps") key = "[CLY]_nps";
+ if (widget.type == "survey") key = "[CLY]_survey";
+ if (widget.type == "rating") key = "[CLY]_star_rating";
+
+ json segmentation;
+ segmentation["platform"] = PLATFORM;
+ segmentation["app_version"] = APP_VERSION;
+ segmentation["widget_id"] = widget.widgetId;
+
+ if (widgetResult.is_null()) {
+ segmentation["closed"] = "1";
+ } else {
+ for (auto& [k, v] : widgetResult.items()) {
+ segmentation[k] = v;
+ }
+ }
+
+ recordEvent(key, segmentation);
+ flushEventQueue();
+}
+
+
+
+Quick Reference
+
+API Endpoints
+
+ | Purpose | Endpoint |
+ | Fetch widget list | /o/sdk?method=feedback&app_key=...&device_id=... |
+ | Fetch widget data | /o/surveys/{type}/widget?widget_id=... |
+ | WebView URL | /feedback/{type}?widget_id=...&custom=... |
+
+
+Custom Parameter Flags
+
+ | Flag | Meaning | When |
+ tc=1 | Terms & Conditions support | Always |
+ xb=1 | WebView handles close button | wv present |
+ rw=1 | Fullscreen display | wv present |
+
+
+Communication URL Patterns
+
+ | Pattern | Action |
+ countly_action_event/?cly_widget_command=1&close=1 | Close WebView, record cancel event |
+ countly_action_event/?cly_x_action_event=1&action=link&link=URL | Open URL in system browser |
+ Any URL with cly_x_int=1 | Open in system browser (T&C) |
+
+
+Suggested Dependencies
+
+ | Library | Purpose |
+ | Qt6 WebEngineWidgets or platform WebView | WebView presentation |
+
+
+
+
diff --git a/examples/qt_demo/README.md b/examples/qt_demo/README.md
new file mode 100644
index 0000000..590ed88
--- /dev/null
+++ b/examples/qt_demo/README.md
@@ -0,0 +1,171 @@
+# Countly C++ SDK Demo App
+
+A Qt6 desktop application for manually testing all features of the Countly C++ SDK, including SDK Behavior Settings (SBS) and a standalone Feedback Widgets flow.
+
+## Prerequisites
+
+| Requirement | Notes |
+| ----------- | ----- |
+| **CMake** | 3.16 or newer |
+| **A C++17 compiler** | AppleClang / Clang / GCC are all fine |
+| **Qt 6** | `Widgets` + `WebEngineWidgets` modules are both required |
+| **libcurl** | Used by the Feedback Widgets tab for direct HTTP calls |
+| **Countly C++ SDK** | Built locally with `-DCOUNTLY_USE_SQLITE=ON` |
+
+### Installing the dependencies on macOS
+
+Homebrew's Qt ships as ~40 keg-only sub-packages (`qtbase`, `qtwebengine`, ...). Qt's own CMake config expects them in a single prefix, so install the unified `qt` formula — it symlinks all the sub-kegs into `/opt/homebrew/opt/qt` where `find_package(Qt6)` can discover every component:
+
+```bash
+brew install cmake qt curl
+```
+
+`brew install qt@6` or installing `qtbase`/`qtwebengine` individually is **not enough** — `find_package(Qt6 REQUIRED COMPONENTS Widgets WebEngineWidgets)` will fail because the per-keg prefixes don't contain each other's config files. If you already have the sub-packages installed, running `brew install qt` is quick; it only adds the symlink farm on top.
+
+### Installing the dependencies on Linux
+
+On Debian/Ubuntu:
+
+```bash
+sudo apt install cmake build-essential libcurl4-openssl-dev \
+ qt6-base-dev qt6-webengine-dev
+```
+
+On Fedora/RHEL:
+
+```bash
+sudo dnf install cmake gcc-c++ libcurl-devel \
+ qt6-qtbase-devel qt6-qtwebengine-devel
+```
+
+## Setup
+
+### 1. Build the Countly C++ SDK
+
+This demo lives inside the SDK repo at `examples/qt_demo/`. It links against `libcountly.dylib` (macOS) / `libcountly.so` (Linux) produced by the SDK's own build, so build the SDK first with SQLite support:
+
+```bash
+cd /path/to/countly-sdk-cpp # the repo root, two levels above this README
+mkdir -p build && cd build
+cmake .. -DCOUNTLY_USE_SQLITE=ON
+make
+```
+
+After this, `countly-sdk-cpp/build/` will contain the Countly shared library. The demo's `CMakeLists.txt` discovers the SDK automatically via `${CMAKE_CURRENT_SOURCE_DIR}/../..` — no path flag needed.
+
+### 2. Create `dev_config.hpp`
+
+The "Load Dev Config" button in the Init tab pulls credentials from this header so you don't have to retype them every run. It's in `.gitignore` — never commit real credentials.
+
+Create `examples/qt_demo/dev_config.hpp` with:
+
+```cpp
+#ifndef DEV_CONFIG_HPP
+#define DEV_CONFIG_HPP
+
+#include
+
+struct DevConfig {
+ std::string serverUrl = "https://your.server.ly";
+ std::string appKey = "YOUR_APP_KEY";
+ std::string deviceId = "test-device-id";
+ std::string dbPath = "countly_demo.db";
+ std::string port = "";
+ std::string salt = "";
+ std::string eqThreshold = "";
+ std::string rqMaxSize = "";
+ std::string rqBatchSize = "";
+ std::string sessionInterval = "";
+ std::string updateInterval = "";
+ std::string metricsOs = "macOS";
+ std::string metricsOsVersion = "15.0";
+ std::string metricsDevice = "MacBook";
+ std::string metricsResolution = "2560x1600";
+ std::string metricsCarrier = "";
+ std::string metricsAppVersion = "1.0";
+ std::string sbsJson = "";
+ bool manualSession = false;
+ bool disableSBSUpdates = false;
+ bool alwaysPost = false;
+ bool enableRemoteConfig = false;
+ bool disableAutoEventsOnUP = false;
+};
+
+#endif
+```
+
+### 3. Configure and build the demo
+
+From the `examples/qt_demo/` folder:
+
+```bash
+mkdir -p build && cd build
+
+cmake \
+ -DCMAKE_PREFIX_PATH=/opt/homebrew/opt/qt \
+ ..
+
+make -j$(sysctl -n hw.ncpu) # on Linux: -j$(nproc)
+```
+
+One flag worth explaining:
+
+- **`CMAKE_PREFIX_PATH`** tells CMake where Qt lives. On macOS with Homebrew this is `/opt/homebrew/opt/qt` (Apple Silicon) or `/usr/local/opt/qt` (Intel). On Linux with distro packages you can usually omit it entirely — the pkg-config files are already on the default search path.
+
+If you keep the SDK checkout somewhere other than this demo's grandparent directory (e.g. a separate working tree), pass `-DCOUNTLY_SDK_DIR=/path/to/countly-sdk-cpp` to override the default.
+
+The binary is written to `build/qt_demo`.
+
+### 4. Run
+
+```bash
+./qt_demo
+```
+
+In the app:
+
+1. Go to **Init / Config**, click **Load Dev Config** (or fill the fields manually), then **Initialize SDK**.
+2. Switch tabs to exercise the feature you want to test. The SDK's log output streams into the panel at the bottom in real time.
+3. For feedback widgets, head to the **Feedback Widgets** tab and click **Fetch Widgets** — it reuses the Init tab's server URL / app key / device ID. Click any widget card to present it in the embedded web view.
+
+## Tabs
+
+The app has 10 tabs covering all SDK features:
+
+| Tab | Description |
+| --- | ----------- |
+| **Init / Config** | Server URL, app key, device ID, metrics, SDK options (manual sessions, SBS JSON, salt, etc.). Initialize and stop the SDK from here. |
+| **Sessions** | Begin, update, and end sessions manually. |
+| **Events** | Send basic events, events with count/sum/duration, and events with custom segmentation. |
+| **Views** | Start and stop named views. |
+| **Crashes** | Record handled exceptions, add breadcrumbs, set custom crash segments. |
+| **User Profile** | Set standard user properties (name, email, etc.) and custom key-value pairs. |
+| **Location** | Set country code, city, GPS coordinates, or IP address. Disable location. |
+| **Device ID** | Change device ID with or without server merge. |
+| **Remote Config** | Fetch all remote config values or fetch specific keys. View the returned JSON. |
+| **Feedback Widgets** | Standalone HTTP flow (does not use the SDK). Fetches feedback widgets from the server and renders them in an embedded `QWebEngineView`, intercepting Countly widget communication URLs. Uses the Server URL / App Key / Device ID from the Init tab. See `Countly_Feedback_Widget_Implementation_Guide.html` for the underlying protocol. |
+
+## Log Panel
+
+A live log panel at the bottom of the window shows all SDK log output in real time. Logs are delivered via a thread-safe Qt signal/slot connection so background SDK threads can log without crashing the UI.
+
+## Troubleshooting
+
+- **`Could NOT find Qt6WebEngineWidgets (missing: Qt6WebEngineWidgets_DIR)`**
+ You're pointing CMake at a Qt prefix that only contains `qtbase`. Install the unified Homebrew `qt` formula (`brew install qt`) or add the `qtwebengine` prefix alongside in `CMAKE_PREFIX_PATH`.
+
+- **Build fails after editing `CMakeLists.txt` or moving the SDK**
+ Delete the `build/` folder and reconfigure. CMake caches `find_package` results; adding a new dependency or changing paths requires a fresh configure, not an incremental rebuild.
+
+- **`libcountly.dylib` not found at launch**
+ The build embeds `${COUNTLY_SDK_DIR}/build` as an `LC_RPATH`. Moving the SDK directory after building invalidates that path. Reconfigure (or patch with `install_name_tool -add_rpath`).
+
+- **WebEngine window is blank / never loads**
+ On macOS the WebEngine process needs network permissions; make sure your firewall isn't blocking `QtWebEngineProcess`. You can also watch the log panel — the demo logs every navigation interception the page makes.
+
+## Notes
+
+- `dev_config.hpp` is in `.gitignore` — never commit credentials.
+- The SDK is linked as a dynamic library. Rebuild the SDK first whenever you update it, then rebuild the demo.
+- The Feedback Widgets tab intentionally bypasses the C++ SDK — it talks to `/o/sdk?method=feedback` and `/feedback/` directly via libcurl. This mirrors the manual protocol documented in `Countly_Feedback_Widget_Implementation_Guide.html` and is useful for validating the server-side widget setup independently of the SDK.
+- On exit the app calls `Countly::getInstance().stop()` before the main window is destroyed so background SDK threads don't call back into a dead GUI.
diff --git a/examples/qt_demo/main.cpp b/examples/qt_demo/main.cpp
new file mode 100644
index 0000000..f56e749
--- /dev/null
+++ b/examples/qt_demo/main.cpp
@@ -0,0 +1,2075 @@
+#include
+#include
+#include
+#include