Skip to content

feat: initial ABsmartly C++ SDK implementation#1

Open
joalves wants to merge 3 commits intomainfrom
feat/initial-sdk
Open

feat: initial ABsmartly C++ SDK implementation#1
joalves wants to merge 3 commits intomainfrom
feat/initial-sdk

Conversation

@joalves
Copy link
Collaborator

@joalves joalves commented Feb 24, 2026

Summary

  • Core C++ SDK with context management and variant assignment
  • Audience matching with JSON expression evaluation engine
  • Hashing utilities (MurmurHash3, MD5) for deterministic assignment
  • Comprehensive test suite covering all SDK components

Test plan

  • Build with CMake and verify compilation
  • Run test suite and verify all tests pass
  • Validate variant assignment matches other SDK implementations

Summary by CodeRabbit

  • New Features

    • C++ SDK for context-based experiment management, treatment selection, variable access, overrides and custom assignments
    • Audience matching, expression evaluation, deterministic variant assignment and hashing utilities
    • Event publishing, context lifecycle (ready/refresh/finalize) and unit/attribute management; CMake build support
  • Documentation

    • Comprehensive README with install, build and getting-started guides
  • Tests

    • Extensive unit and integration test suite covering evaluator, hashing, assigner, audience matching and context behaviour

Core SDK with context management, variant assignment, audience
matching, JSON expression evaluation, and comprehensive test suite.
@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Walkthrough

This PR adds a complete C++ SDK for A/B Smartly: a CMake build (C++17) with FetchContent for nlohmann::json and optional CURL, a public library target absmartly-sdk, many new public headers (models, context, config, SDK, hashing, variant assigner, audience matcher, JSON expression evaluator and operators, errors, event handler) and their implementations (MD5, Murmur3, utils, variant assigner, audience matcher, context, JSON expression evaluator and operators). It also adds a comprehensive Catch2-based test suite and README documentation.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 In code-hopped fields I dig and play,

MD5 crumbs and Murmur3 hay,
Context gardens bloom with rules,
JSON logic polishes the tools,
Variants picked—hip-hop hooray!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: initial ABsmartly C++ SDK implementation' clearly and specifically describes the main change—introducing a complete C++ SDK implementation for ABsmartly, which aligns with the extensive changeset across all components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/initial-sdk

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (17)
tests/CMakeLists.txt (1)

1-4: Add URL_HASH to FetchContent_Declare for integrity verification.

Adding a SHA-256 hash ensures reproducible fetches and guards against tampering. The hash value for the Catch2 v3.5.2 tarball should be computed and added:

🔒 Suggested change
 FetchContent_Declare(
   Catch2
   URL https://github.com/catchorg/Catch2/archive/refs/tags/v3.5.2.tar.gz
+  URL_HASH SHA256=<compute via: curl -sL "https://github.com/catchorg/Catch2/archive/refs/tags/v3.5.2.tar.gz" | sha256sum>
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/CMakeLists.txt` around lines 1 - 4, The FetchContent_Declare block for
Catch2 (the call to FetchContent_Declare with the Catch2 target and its URL) is
missing an integrity hash; compute the SHA-256 of the v3.5.2 tarball and add a
URL_HASH entry to that FetchContent_Declare (use the CMake form URL_HASH
SHA256=<computed-hash>) so the Catch2 declaration includes the integrity check.
CMakeLists.txt (1)

9-12: Add an integrity hash for the fetched nlohmann_json archive.

Using URL_HASH helps prevent supply‑chain drift if the tarball changes. Compute the SHA‑256 for the release artefact and pin it here.

🔐 Suggested pinning
 FetchContent_Declare(
   json
   URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
+  URL_HASH SHA256=<sha256>
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CMakeLists.txt` around lines 9 - 12, The FetchContent_Declare block for the
dependency named "json" currently fetches the release tarball without integrity
verification; compute the SHA-256 of the v3.11.3 json.tar.xz release artifact
and add a URL_HASH entry to the FetchContent_Declare for the "json" target
(i.e., the FetchContent_Declare(...) that references json and URL) using the
format expected by CMake so the fetch is pinned to that SHA-256 digest.
src/utils.cpp (1)

55-64: Make empty split handling explicit.

Line 63 currently relies on split.size() - 1, which underflows to ‑1 when the split is empty. An explicit guard clarifies the contract and avoids accidental indexing by callers.

♻️ Suggested guard
 int choose_variant(const std::vector<double>& split, double probability) {
+    if (split.empty()) {
+        return -1;
+    }
     double cum_sum = 0.0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils.cpp` around lines 55 - 64, The function choose_variant currently
underflows when split is empty; add an explicit guard at the start of
choose_variant that checks split.empty() and fails fast (e.g., throw
std::invalid_argument("choose_variant: split must not be empty")) so callers
can't accidentally rely on split.size()-1; update error message to include the
function name and ensure the header <stdexcept> is included where appropriate.
src/context.cpp (5)

14-20: array_equals_shallow is equivalent to std::vector::operator==.

std::vector<double>::operator== already performs element-wise comparison. This helper can be replaced by a == b.

♻️ Proposed simplification
-static bool array_equals_shallow(const std::vector<double>& a, const std::vector<double>& b) {
-    if (a.size() != b.size()) return false;
-    for (size_t i = 0; i < a.size(); ++i) {
-        if (a[i] != b[i]) return false;
-    }
-    return true;
-}

Then at the call site (line 654):

-        && array_equals_shallow(experiment.trafficSplit, assignment.traffic_split);
+        && experiment.trafficSplit == assignment.traffic_split;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 14 - 20, The helper function
array_equals_shallow duplicates std::vector<double>::operator==; remove the
array_equals_shallow definition and replace every call site (e.g., occurrences
where array_equals_shallow(a,b) is used) with the direct comparison (a == b)
using the original vector variables; ensure headers remain for std::vector and
that no other code expects a non-boolean behavior from array_equals_shallow
before deleting it.

40-41: Redundant data_ re-assignment in init().

The constructor already initialises data_ via the member initialiser list (line 25: data_(std::move(data))), then calls init(data_) which re-assigns data_ = data (line 575). This copies data_ back onto itself — harmless but wasteful. Consider having init only call build_index(), or removing the redundant assignment.

♻️ Proposed fix
 void Context::init(const ContextData& data) {
-    data_ = data;
     build_index();
 }

Or inline the call in the constructor:

-    init(data_);
+    build_index();

Also applies to: 574-577

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 40 - 41, The constructor sets data_ via the
member initializer list then immediately calls init(data_) which reassigns data_
to itself; remove the redundant assignment by changing init to not reassign
data_ (i.e., drop `data_ = data` inside init) and have init only call
build_index(), or alternatively change the constructor to call build_index()
directly instead of init(data_); update references to init() and the constructor
accordingly (symbols: init, build_index, data_).

309-337: track() only stores numeric properties in GoalAchievement but emits all properties in the event.

Lines 318–321 filter goal.properties to numeric values only, but line 330 emits the original properties JSON (including non-numeric values) in the event payload. If this is intentional (event handler sees everything, but the publish payload is filtered), it's worth a brief comment to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 309 - 337, Context::track currently stores only
numeric entries into GoalAchievement::properties but emits the original
properties JSON to emit_event, causing a mismatch; change the emission to
reflect the filtered stored properties by converting goal.properties into a
nlohmann::json (or null when empty) and assign that to goal_data["properties"]
before calling emit_event("goal", goal_data) so the event payload matches the
stored GoalAchievement, referencing Context::track, GoalAchievement::properties,
and emit_event.

412-540: get_or_create_assignment — correct but dense; consider extracting sub-functions.

This 130-line method handles cache invalidation, overrides, audience matching, traffic eligibility, custom assignments, full-on variants, and variable resolution. Breaking it into smaller helpers (e.g., compute_assignment_for_experiment, apply_override) would improve readability and testability without changing behaviour.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 412 - 540, The get_or_create_assignment
function is too large and should be split into smaller helpers to improve
readability and testability; refactor by extracting the distinct
responsibilities into methods such as apply_override(Assignment&, const
ExperimentIndex*, const std::string& experiment_name), evaluate_audience(const
ExperimentData&, Assignment&), determine_variant_and_eligibility(const
ExperimentData&, Assignment&, const std::string& unit_type), and
resolve_variables_if_applicable(Assignment&, const ExperimentIndex*), then
rewrite get_or_create_assignment to perform high-level flow: check
cache/overrides, call apply_override when overrides_ exists, otherwise call
evaluate_audience then determine_variant_and_eligibility, set meta fields (id,
iteration, traffic_split, attrs_seq), call resolve_variables_if_applicable, and
finally store/move the Assignment into assignments_ so behavior remains
identical but logic is modularized (use existing symbols:
get_or_create_assignment, assignments_, overrides_, index_, audience_matcher_,
assigners_, units_, cassignments_).

616-646: Manual sorted insertion in build_index could be simplified.

The nested loop with manual insertion-point search for index_variables_ can be replaced with std::lower_bound for clarity and to avoid the duplicate-check logic interleaved with insertion.

♻️ Simplified with std::lower_bound
-                if (!found) {
-                    bool inserted = false;
-                    for (auto it = vec.begin(); it != vec.end(); ++it) {
-                        if (entry.data->id < (*it)->data->id) {
-                            vec.insert(it, &entry);
-                            inserted = true;
-                            break;
-                        }
-                        if (entry.data->id == (*it)->data->id) {
-                            inserted = true;
-                            break;
-                        }
-                    }
-                    if (!inserted) {
-                        vec.push_back(&entry);
-                    }
-                }
+                if (!found) {
+                    auto pos = std::lower_bound(
+                        vec.begin(), vec.end(), &entry,
+                        [](const ExperimentIndex* a, const ExperimentIndex* b) {
+                            return a->data->id < b->data->id;
+                        });
+                    if (pos == vec.end() || (*pos)->data->id != entry.data->id) {
+                        vec.insert(pos, &entry);
+                    }
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 616 - 646, The manual sorted insertion in
build_index that walks vec to insert &entry (using index_variables_, index_, and
entry) should be replaced with std::lower_bound to find the insertion position
and a single duplicate check; for each key get auto& vec =
index_variables_[key], use std::lower_bound(vec.begin(), vec.end(), &entry,
[](const ContextEntry* a, const ContextEntry* b){ return a->data->id <
b->data->id; }) to obtain the iterator, then if the iterator is at end or
(*it)->data->id != entry.data->id (or pointer inequality) call vec.insert(it,
&entry); this removes the manual loop/flags and requires adding `#include`
<algorithm> and using the existing symbols index_variables_, entry, index_, and
build_index.
src/json_expr/evaluator.cpp (3)

4-4: Unused include <charconv>.

This header is included but never used — the implementation uses std::stod and std::ostringstream instead of std::from_chars/std::to_chars.

🧹 Remove unused include
 `#include` <cmath>
-#include <charconv>
 `#include` <sstream>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/json_expr/evaluator.cpp` at line 4, Remove the unused `#include` <charconv>
from src/json_expr/evaluator.cpp: the implementation uses std::stod and
std::ostringstream (see functions around evaluator.cpp where std::stod and
std::ostringstream are used), so delete the <charconv> include to avoid an
unnecessary dependency and compilation warning.

35-43: Object expression dispatch relies on JSON key iteration order.

The loop iterates the object's keys, checks only the first key against the operator map, and immediately breaks. This is intentional for the single-key expression format, but nlohmann::json objects iterate in alphabetical order (they use std::map internally by default). This means {"gt": [...], "value": 5} would match "gt" rather than "value", which is fine — but it's worth a comment to make the assumption explicit for maintainers.

📝 Suggested inline comment
     if (expr.is_object()) {
+        // Expression objects must contain exactly one operator key.
+        // We check only the first key (alphabetical order in nlohmann::json).
         for (auto it = expr.begin(); it != expr.end(); ++it) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/json_expr/evaluator.cpp` around lines 35 - 43, The object-dispatch in
Evaluator::evaluate relies on iterating a nlohmann::json object and using only
the first key to find an operator in operators_, which depends on
nlohmann::json's alphabetical iteration order and the convention that
expressions are single-key objects; update the evaluate function to include an
explicit inline comment documenting that assumption (mention nlohmann::json uses
std::map and iterates keys alphabetically), and optionally add a small runtime
check (e.g., assert or throw) that the object has exactly one key before using
the first key to select the operator to make the contract explicit; reference
the evaluate method and the operators_ lookup in your change.

139-148: Direct floating-point equality comparison in compare.

lv == rv at line 144 uses direct double equality. For the intended use case (small integers and simple decimals in A/B testing config), this is acceptable. Just flagging that an epsilon-based comparison would be more robust if fractional arithmetic ever feeds into comparison operands.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/json_expr/evaluator.cpp` around lines 139 - 148, The compare logic in
function compare uses direct double equality (lv == rv) which is fragile; change
it to an epsilon-based comparison: compute if (std::fabs(lv - rv) < EPS) treat
as equal, else return lv > rv ? 1 : -1; define a small constant EPS (e.g. 1e-9)
scoped to the file or function and include <cmath> or use std::abs, and update
the branch in evaluator.cpp where lv and rv are compared to use this epsilon
comparison.
src/json_expr/operators.cpp (2)

243-275: std::regex is compiled on every evaluation — acceptable but worth noting.

std::regex construction is expensive. If MatchOperator is evaluated repeatedly with the same pattern (e.g., during bulk audience matching), this could become a hot path. Not critical for typical A/B testing workloads, but a regex cache could be added later if profiling shows contention.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/json_expr/operators.cpp` around lines 243 - 275, MatchOperator::evaluate
currently constructs std::regex every call which is expensive; add a simple
cache of compiled regex objects keyed by the pattern string in the MatchOperator
class (e.g., an std::unordered_map<std::string, std::regex> member) and
lookup/insert before constructing a new std::regex inside
MatchOperator::evaluate; protect the map with a mutex or shared_mutex for
thread-safety when reading/writing and reuse the cached std::regex for
std::regex_search, falling back to compiling and caching on regex construction
errors (same error handling as today).

104-194: Significant code duplication across Gt, Gte, Lt, Lte operators.

These four operators share identical boilerplate (array check, lhs/rhs evaluation, null guards, compare call) and differ only in the comparison predicate. Extracting a shared helper would reduce ~90 lines to ~20 and make adding new comparison operators trivial.

♻️ Proposed helper-based refactor
+// In a shared location (e.g., anonymous namespace in operators.cpp):
+static nlohmann::json compare_op(
+    const Evaluator& evaluator,
+    const nlohmann::json& args,
+    const nlohmann::json& vars,
+    std::function<bool(int)> predicate
+) {
+    if (!args.is_array()) return nullptr;
+    auto lhs = args.size() > 0 ? evaluator.evaluate(args[0], vars) : nlohmann::json(nullptr);
+    if (lhs.is_null()) return nullptr;
+    auto rhs = args.size() > 1 ? evaluator.evaluate(args[1], vars) : nlohmann::json(nullptr);
+    if (rhs.is_null()) return nullptr;
+    auto result = Evaluator::compare(lhs, rhs);
+    if (result.has_value()) return predicate(*result);
+    return nullptr;
+}
+
 nlohmann::json GtOperator::evaluate(
     const Evaluator& evaluator,
     const nlohmann::json& args,
     const nlohmann::json& vars
 ) const {
-    if (!args.is_array()) {
-        return nullptr;
-    }
-    auto lhs = args.size() > 0 ? evaluator.evaluate(args[0], vars) : nlohmann::json(nullptr);
-    if (lhs.is_null()) {
-        return nullptr;
-    }
-    auto rhs = args.size() > 1 ? evaluator.evaluate(args[1], vars) : nlohmann::json(nullptr);
-    if (rhs.is_null()) {
-        return nullptr;
-    }
-    auto result = Evaluator::compare(lhs, rhs);
-    if (result.has_value()) {
-        return *result > 0;
-    }
-    return nullptr;
+    return compare_op(evaluator, args, vars, [](int r) { return r > 0; });
 }

Apply similarly to GteOperator, LtOperator, and LteOperator.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/json_expr/operators.cpp` around lines 104 - 194, The four evaluate
methods (GtOperator::evaluate, GteOperator::evaluate, LtOperator::evaluate,
LteOperator::evaluate) duplicate the same argument checking, lhs/rhs evaluation,
null guards and call to Evaluator::compare; extract a shared helper (e.g.,
evaluateComparison or evaluateBinaryComparison) that takes (const Evaluator&
evaluator, const nlohmann::json& args, const nlohmann::json& vars,
std::function<bool(int)> predicate) and encapsulates: array check, evaluate
lhs/rhs, null checks, call Evaluator::compare and return predicate(result) as
nlohmann::json or nullptr on failure; then rewrite each operator
(GtOperator::evaluate, GteOperator::evaluate, LtOperator::evaluate,
LteOperator::evaluate) to call this helper with the appropriate lambda (r>0,
r>=0, r<0, r<=0).
include/absmartly/context.h (2)

16-16: <chrono> is unused in this header.

std::chrono is used only in context.cpp (via now_millis). Moving it to the implementation file reduces transitive include costs.

🧹 Move to implementation file
-#include <chrono>

And ensure #include <chrono> is present in src/context.cpp (it already is at line 5).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/absmartly/context.h` at line 16, Remove the unused header include
<chrono> from include/absmartly/context.h because std::chrono is only used in
the implementation (now_millis in context.cpp); ensure `#include` <chrono> remains
(or is added) in src/context.cpp so the implementation compiles, and run a quick
build to confirm no transitive include breakage.

20-42: Assignment and ExperimentIndex are implementation details leaked into the public header.

These structs are used internally by Context (private members) and are not part of the public API surface. Exposing them in the header couples consumers to internal representation. Consider forward-declaring them and defining them in an internal header or in context.cpp.

This is fine for an initial implementation and can be deferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/absmartly/context.h` around lines 20 - 42, The public header is
leaking internal implementation details by defining struct Assignment and struct
ExperimentIndex; instead, forward-declare these types in the public header and
move their full definitions into an internal header or context.cpp. Update
Context's private members to use opaque pointers (e.g.,
std::unique_ptr<Assignment>/ExperimentIndex or raw pointers) so the compiler
only needs the forward declarations in include/absmartly/context.h, and add the
moved definitions and any required includes (nlohmann::json, vector, map,
ExperimentData) to the internal header or context.cpp where Context's methods
are implemented.
include/absmartly/json_expr/operators.h (1)

3-6: Remove <regex> from the header — it is only used in the .cpp implementation.

<regex> is a notoriously heavy header and is only needed by MatchOperator::evaluate in operators.cpp. Including it here inflates compile times for every translation unit that includes operators.h.

🧹 Proposed fix
 `#include` <memory>
 `#include` <optional>
-#include <regex>
 `#include` <string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/absmartly/json_expr/operators.h` around lines 3 - 6, The header
operators.h unnecessarily includes <regex>, which increases compile times;
remove the `#include` <regex> from operators.h and ensure the implementation file
(operators.cpp) includes <regex> so MatchOperator::evaluate has the needed
declaration; keep other headers (<memory>, <optional>, <string>) in operators.h
and verify no other declarations in the header depend on std::regex.
tests/json_expr_test.cpp (1)

332-416: Good integration tests — consider adding edge-case coverage for null operator and in/match operators.

The integration tests effectively validate composite audience-matching expressions. You may want to add integration-level tests for the null, in, and match operators as well, since they are registered in the Evaluator but not exercised here (they may be tested in operators_test.cpp, but integration-level coverage would strengthen confidence).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/json_expr_test.cpp` around lines 332 - 416, Add integration tests that
exercise the registered 'null', 'in', and 'match' operators by creating new
SECTIONs that call Evaluator::evaluate_boolean with appropriate json expressions
built using the existing helpers (value_for, var_for, binary_op, unary_op) and
the same sample subjects (john, terry, kate, maria); for 'null' construct an
expression that checks a missing or explicitly null field, for 'in' use
binary_op("in", var_for("language"), value_for(json::array({...}))) to assert
membership, and for 'match' use binary_op("match", var_for("language"),
value_for("regex-or-pattern")) to validate pattern matching — reuse the existing
test structure and REQUIRE assertions similar to other SECTIONs to verify
expected boolean results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@include/absmartly/models.h`:
- Around line 42-47: The from_json deserializer for CustomFieldValue can leave
stale data in CustomFieldValue::value when the JSON "value" key is missing or
null; update inline void from_json(const nlohmann::json& j, CustomFieldValue& v)
to explicitly reset v.value to std::nullopt in the else path (i.e., when
!j.contains("value") or j.at("value").is_null()), leaving the existing get logic
when a non-null value is present so existing instances get cleared correctly.

In `@README.md`:
- Around line 11-37: The Installation section in README.md uses H2 then jumps to
H4 (the "#### CMake (FetchContent)", "#### CMake (Local)", and "#### Building
from Source" headings); update those subsection headings to H3 (change #### to
###) so heading levels increment properly and satisfy markdownlint's
heading-increment rule—ensure all related subsection headings under the
Installation H2 are consistently H3.
- Around line 185-187: Add a comma after "logs in" in the sentence describing
when to call set_unit() or set_units() to improve readability; update the README
sentence mentioning set_unit(), set_units() and context so it reads "...when a
user logs in, and you want to associate..." (keep references to set_unit(),
set_units(), and context unchanged).

In `@src/context.cpp`:
- Around line 273-278: The code in custom_field_value uses std::stod (in the
block handling field.type == "number"), which is locale-dependent; replace the
std::stod call with a locale-independent parser such as std::from_chars (include
<charconv>), parse val into a double using std::from_chars, check the returned
error code (std::errc or std::from_chars_result) and return nullptr on failure
(same behavior as the current catch path), and remove the try/catch; update the
custom_field_value function to handle the from_chars result and preserve the
existing return semantics.
- Around line 153-155: set_override currently mutates overrides_ after
finalisation while set_custom_assignment enforces immutability via
check_not_finalized(); call check_not_finalized() at the start of both
Context::set_override and Context::set_overrides to prevent post-finalisation
mutation and keep behavior consistent with set_custom_assignment (or, if
overrides are intentionally mutable, add explicit documentation to that effect
instead). Ensure you reference the check_not_finalized() guard and the
overrides_ mutations in these methods so tests expecting post-finalisation
mutation to be disallowed will pass.

In `@src/json_expr/evaluator.cpp`:
- Around line 112-124: The number-to-string path in src/json_expr/evaluator.cpp
uses std::ostringstream (the two local variables named oss) which pick up the
global locale; imbue each ostringstream with the C locale (call
oss.imbue(std::locale::classic()) immediately after construction) before
formatting so decimal separators are always '.'; also ensure <locale> is
included if not already.
- Around line 84-100: The string-to-double conversion uses locale-sensitive
std::stod; replace it with locale-independent std::from_chars (include
<charconv>) to parse the std::string 'str' (or a std::string_view) into a double
named 'result', remove the try/catch and the pos variable, then check that
from_chars returned no error and consumed the entire input (the end pointer
equals str.data()+str.size()) and that std::isfinite(result) before returning
result; otherwise return std::nullopt — update the conversion logic in the block
that currently references value, str, pos, result to use std::from_chars
instead.

In `@src/md5.cpp`:
- Around line 169-174: The code currently writes only a 32-bit length into
block[14], truncating inputs ≥512MB; compute the full 64-bit bit-length (e.g.
uint64_t bitlen = static_cast<uint64_t>(len) << 3), then store the low 32 bits
into block[14] and the high 32 bits into block[15] before calling
md5cycle(state, block) so the MD5 length is encoded as a 64-bit value.

---

Nitpick comments:
In `@CMakeLists.txt`:
- Around line 9-12: The FetchContent_Declare block for the dependency named
"json" currently fetches the release tarball without integrity verification;
compute the SHA-256 of the v3.11.3 json.tar.xz release artifact and add a
URL_HASH entry to the FetchContent_Declare for the "json" target (i.e., the
FetchContent_Declare(...) that references json and URL) using the format
expected by CMake so the fetch is pinned to that SHA-256 digest.

In `@include/absmartly/context.h`:
- Line 16: Remove the unused header include <chrono> from
include/absmartly/context.h because std::chrono is only used in the
implementation (now_millis in context.cpp); ensure `#include` <chrono> remains (or
is added) in src/context.cpp so the implementation compiles, and run a quick
build to confirm no transitive include breakage.
- Around line 20-42: The public header is leaking internal implementation
details by defining struct Assignment and struct ExperimentIndex; instead,
forward-declare these types in the public header and move their full definitions
into an internal header or context.cpp. Update Context's private members to use
opaque pointers (e.g., std::unique_ptr<Assignment>/ExperimentIndex or raw
pointers) so the compiler only needs the forward declarations in
include/absmartly/context.h, and add the moved definitions and any required
includes (nlohmann::json, vector, map, ExperimentData) to the internal header or
context.cpp where Context's methods are implemented.

In `@include/absmartly/json_expr/operators.h`:
- Around line 3-6: The header operators.h unnecessarily includes <regex>, which
increases compile times; remove the `#include` <regex> from operators.h and ensure
the implementation file (operators.cpp) includes <regex> so
MatchOperator::evaluate has the needed declaration; keep other headers
(<memory>, <optional>, <string>) in operators.h and verify no other declarations
in the header depend on std::regex.

In `@src/context.cpp`:
- Around line 14-20: The helper function array_equals_shallow duplicates
std::vector<double>::operator==; remove the array_equals_shallow definition and
replace every call site (e.g., occurrences where array_equals_shallow(a,b) is
used) with the direct comparison (a == b) using the original vector variables;
ensure headers remain for std::vector and that no other code expects a
non-boolean behavior from array_equals_shallow before deleting it.
- Around line 40-41: The constructor sets data_ via the member initializer list
then immediately calls init(data_) which reassigns data_ to itself; remove the
redundant assignment by changing init to not reassign data_ (i.e., drop `data_ =
data` inside init) and have init only call build_index(), or alternatively
change the constructor to call build_index() directly instead of init(data_);
update references to init() and the constructor accordingly (symbols: init,
build_index, data_).
- Around line 309-337: Context::track currently stores only numeric entries into
GoalAchievement::properties but emits the original properties JSON to
emit_event, causing a mismatch; change the emission to reflect the filtered
stored properties by converting goal.properties into a nlohmann::json (or null
when empty) and assign that to goal_data["properties"] before calling
emit_event("goal", goal_data) so the event payload matches the stored
GoalAchievement, referencing Context::track, GoalAchievement::properties, and
emit_event.
- Around line 412-540: The get_or_create_assignment function is too large and
should be split into smaller helpers to improve readability and testability;
refactor by extracting the distinct responsibilities into methods such as
apply_override(Assignment&, const ExperimentIndex*, const std::string&
experiment_name), evaluate_audience(const ExperimentData&, Assignment&),
determine_variant_and_eligibility(const ExperimentData&, Assignment&, const
std::string& unit_type), and resolve_variables_if_applicable(Assignment&, const
ExperimentIndex*), then rewrite get_or_create_assignment to perform high-level
flow: check cache/overrides, call apply_override when overrides_ exists,
otherwise call evaluate_audience then determine_variant_and_eligibility, set
meta fields (id, iteration, traffic_split, attrs_seq), call
resolve_variables_if_applicable, and finally store/move the Assignment into
assignments_ so behavior remains identical but logic is modularized (use
existing symbols: get_or_create_assignment, assignments_, overrides_, index_,
audience_matcher_, assigners_, units_, cassignments_).
- Around line 616-646: The manual sorted insertion in build_index that walks vec
to insert &entry (using index_variables_, index_, and entry) should be replaced
with std::lower_bound to find the insertion position and a single duplicate
check; for each key get auto& vec = index_variables_[key], use
std::lower_bound(vec.begin(), vec.end(), &entry, [](const ContextEntry* a, const
ContextEntry* b){ return a->data->id < b->data->id; }) to obtain the iterator,
then if the iterator is at end or (*it)->data->id != entry.data->id (or pointer
inequality) call vec.insert(it, &entry); this removes the manual loop/flags and
requires adding `#include` <algorithm> and using the existing symbols
index_variables_, entry, index_, and build_index.

In `@src/json_expr/evaluator.cpp`:
- Line 4: Remove the unused `#include` <charconv> from
src/json_expr/evaluator.cpp: the implementation uses std::stod and
std::ostringstream (see functions around evaluator.cpp where std::stod and
std::ostringstream are used), so delete the <charconv> include to avoid an
unnecessary dependency and compilation warning.
- Around line 35-43: The object-dispatch in Evaluator::evaluate relies on
iterating a nlohmann::json object and using only the first key to find an
operator in operators_, which depends on nlohmann::json's alphabetical iteration
order and the convention that expressions are single-key objects; update the
evaluate function to include an explicit inline comment documenting that
assumption (mention nlohmann::json uses std::map and iterates keys
alphabetically), and optionally add a small runtime check (e.g., assert or
throw) that the object has exactly one key before using the first key to select
the operator to make the contract explicit; reference the evaluate method and
the operators_ lookup in your change.
- Around line 139-148: The compare logic in function compare uses direct double
equality (lv == rv) which is fragile; change it to an epsilon-based comparison:
compute if (std::fabs(lv - rv) < EPS) treat as equal, else return lv > rv ? 1 :
-1; define a small constant EPS (e.g. 1e-9) scoped to the file or function and
include <cmath> or use std::abs, and update the branch in evaluator.cpp where lv
and rv are compared to use this epsilon comparison.

In `@src/json_expr/operators.cpp`:
- Around line 243-275: MatchOperator::evaluate currently constructs std::regex
every call which is expensive; add a simple cache of compiled regex objects
keyed by the pattern string in the MatchOperator class (e.g., an
std::unordered_map<std::string, std::regex> member) and lookup/insert before
constructing a new std::regex inside MatchOperator::evaluate; protect the map
with a mutex or shared_mutex for thread-safety when reading/writing and reuse
the cached std::regex for std::regex_search, falling back to compiling and
caching on regex construction errors (same error handling as today).
- Around line 104-194: The four evaluate methods (GtOperator::evaluate,
GteOperator::evaluate, LtOperator::evaluate, LteOperator::evaluate) duplicate
the same argument checking, lhs/rhs evaluation, null guards and call to
Evaluator::compare; extract a shared helper (e.g., evaluateComparison or
evaluateBinaryComparison) that takes (const Evaluator& evaluator, const
nlohmann::json& args, const nlohmann::json& vars, std::function<bool(int)>
predicate) and encapsulates: array check, evaluate lhs/rhs, null checks, call
Evaluator::compare and return predicate(result) as nlohmann::json or nullptr on
failure; then rewrite each operator (GtOperator::evaluate,
GteOperator::evaluate, LtOperator::evaluate, LteOperator::evaluate) to call this
helper with the appropriate lambda (r>0, r>=0, r<0, r<=0).

In `@src/utils.cpp`:
- Around line 55-64: The function choose_variant currently underflows when split
is empty; add an explicit guard at the start of choose_variant that checks
split.empty() and fails fast (e.g., throw std::invalid_argument("choose_variant:
split must not be empty")) so callers can't accidentally rely on split.size()-1;
update error message to include the function name and ensure the header
<stdexcept> is included where appropriate.

In `@tests/CMakeLists.txt`:
- Around line 1-4: The FetchContent_Declare block for Catch2 (the call to
FetchContent_Declare with the Catch2 target and its URL) is missing an integrity
hash; compute the SHA-256 of the v3.5.2 tarball and add a URL_HASH entry to that
FetchContent_Declare (use the CMake form URL_HASH SHA256=<computed-hash>) so the
Catch2 declaration includes the integrity check.

In `@tests/json_expr_test.cpp`:
- Around line 332-416: Add integration tests that exercise the registered
'null', 'in', and 'match' operators by creating new SECTIONs that call
Evaluator::evaluate_boolean with appropriate json expressions built using the
existing helpers (value_for, var_for, binary_op, unary_op) and the same sample
subjects (john, terry, kate, maria); for 'null' construct an expression that
checks a missing or explicitly null field, for 'in' use binary_op("in",
var_for("language"), value_for(json::array({...}))) to assert membership, and
for 'match' use binary_op("match", var_for("language"),
value_for("regex-or-pattern")) to validate pattern matching — reuse the existing
test structure and REQUIRE assertions similar to other SECTIONs to verify
expected boolean results.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c547139 and b83f1e4.

📒 Files selected for processing (30)
  • CMakeLists.txt
  • README.md
  • include/absmartly/audience_matcher.h
  • include/absmartly/context.h
  • include/absmartly/context_config.h
  • include/absmartly/context_event_handler.h
  • include/absmartly/errors.h
  • include/absmartly/hashing.h
  • include/absmartly/json_expr/evaluator.h
  • include/absmartly/json_expr/operators.h
  • include/absmartly/models.h
  • include/absmartly/sdk.h
  • include/absmartly/variant_assigner.h
  • src/audience_matcher.cpp
  • src/context.cpp
  • src/json_expr/evaluator.cpp
  • src/json_expr/operators.cpp
  • src/md5.cpp
  • src/murmur3.cpp
  • src/utils.cpp
  • src/variant_assigner.cpp
  • tests/CMakeLists.txt
  • tests/audience_matcher_test.cpp
  • tests/context_test.cpp
  • tests/json_expr_test.cpp
  • tests/md5_test.cpp
  • tests/murmur3_test.cpp
  • tests/operators_test.cpp
  • tests/utils_test.cpp
  • tests/variant_assigner_test.cpp

Comment on lines +42 to +47
inline void from_json(const nlohmann::json& j, CustomFieldValue& v) {
j.at("name").get_to(v.name);
j.at("type").get_to(v.type);
if (j.contains("value") && !j.at("value").is_null()) {
v.value = j.at("value").get<std::string>();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reset CustomFieldValue::value when JSON is null or missing.

If from_json is called on an existing instance, a null/missing value leaves stale data in v.value. Resetting to std::nullopt avoids that.

🛠️ Suggested fix
 inline void from_json(const nlohmann::json& j, CustomFieldValue& v) {
     j.at("name").get_to(v.name);
     j.at("type").get_to(v.type);
     if (j.contains("value") && !j.at("value").is_null()) {
         v.value = j.at("value").get<std::string>();
+    } else {
+        v.value = std::nullopt;
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
inline void from_json(const nlohmann::json& j, CustomFieldValue& v) {
j.at("name").get_to(v.name);
j.at("type").get_to(v.type);
if (j.contains("value") && !j.at("value").is_null()) {
v.value = j.at("value").get<std::string>();
}
inline void from_json(const nlohmann::json& j, CustomFieldValue& v) {
j.at("name").get_to(v.name);
j.at("type").get_to(v.type);
if (j.contains("value") && !j.at("value").is_null()) {
v.value = j.at("value").get<std::string>();
} else {
v.value = std::nullopt;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/absmartly/models.h` around lines 42 - 47, The from_json deserializer
for CustomFieldValue can leave stale data in CustomFieldValue::value when the
JSON "value" key is missing or null; update inline void from_json(const
nlohmann::json& j, CustomFieldValue& v) to explicitly reset v.value to
std::nullopt in the else path (i.e., when !j.contains("value") or
j.at("value").is_null()), leaving the existing get logic when a non-null value
is present so existing instances get cleared correctly.

Comment on lines +11 to +37
#### CMake (FetchContent)

Add the following to your `CMakeLists.txt` to include the SDK directly from the repository:

```cmake
include(FetchContent)

FetchContent_Declare(
absmartly-sdk
GIT_REPOSITORY https://github.com/absmartly/cpp-sdk.git
GIT_TAG main
)
FetchContent_MakeAvailable(absmartly-sdk)

target_link_libraries(your_target PRIVATE absmartly-sdk)
```

#### CMake (Local)

If you have cloned the repository locally, you can add it as a subdirectory:

```cmake
add_subdirectory(path/to/cpp-sdk)
target_link_libraries(your_target PRIVATE absmartly-sdk)
```

#### Building from Source
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix heading level jumps in the Installation section.
Line 11 (and the following subsection headings) jump from H2 to H4, which triggers markdownlint’s heading increment rule. Consider using H3 for these subsections.

Proposed change
-#### CMake (FetchContent)
+### CMake (FetchContent)
@@
-#### CMake (Local)
+### CMake (Local)
@@
-#### Building from Source
+### Building from Source
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#### CMake (FetchContent)
Add the following to your `CMakeLists.txt` to include the SDK directly from the repository:
```cmake
include(FetchContent)
FetchContent_Declare(
absmartly-sdk
GIT_REPOSITORY https://github.com/absmartly/cpp-sdk.git
GIT_TAG main
)
FetchContent_MakeAvailable(absmartly-sdk)
target_link_libraries(your_target PRIVATE absmartly-sdk)
```
#### CMake (Local)
If you have cloned the repository locally, you can add it as a subdirectory:
```cmake
add_subdirectory(path/to/cpp-sdk)
target_link_libraries(your_target PRIVATE absmartly-sdk)
```
#### Building from Source
### CMake (FetchContent)
Add the following to your `CMakeLists.txt` to include the SDK directly from the repository:
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 11-11: Heading levels should only increment by one level at a time
Expected: h3; Actual: h4

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 11 - 37, The Installation section in README.md uses
H2 then jumps to H4 (the "#### CMake (FetchContent)", "#### CMake (Local)", and
"#### Building from Source" headings); update those subsection headings to H3
(change #### to ###) so heading levels increment properly and satisfy
markdownlint's heading-increment rule—ensure all related subsection headings
under the Installation H2 are consistently H3.

Comment on lines +185 to +187
### Setting Extra Units

You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in and you want to associate the new identity with the context. Note that you cannot override an already set unit type, as that would be a change of identity and will throw an exception. In this case, you must create a new context instead.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor punctuation tweak for readability.
Line 187 reads more smoothly with a comma after “logs in”.

Proposed change
-You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in and you want to associate the new identity with the context.
+You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in, and you want to associate the new identity with the context.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Setting Extra Units
You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in and you want to associate the new identity with the context. Note that you cannot override an already set unit type, as that would be a change of identity and will throw an exception. In this case, you must create a new context instead.
### Setting Extra Units
You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in, and you want to associate the new identity with the context. Note that you cannot override an already set unit type, as that would be a change of identity and will throw an exception. In this case, you must create a new context instead.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~187-~187: Use a comma before ‘and’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...hods. This is useful when a user logs in and you want to associate the new identity ...

(COMMA_COMPOUND_SENTENCE)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 185 - 187, Add a comma after "logs in" in the
sentence describing when to call set_unit() or set_units() to improve
readability; update the README sentence mentioning set_unit(), set_units() and
context so it reads "...when a user logs in, and you want to associate..." (keep
references to set_unit(), set_units(), and context unchanged).

Comment on lines +153 to +155
void Context::set_override(const std::string& experiment_name, int variant) {
overrides_[experiment_name] = variant;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/context.cpp | sed -n '145,175p'

Repository: absmartly/cpp-sdk

Length of output: 1248


🏁 Script executed:

rg -n "check_not_finalized" --type=cpp -B2 -A2

Repository: absmartly/cpp-sdk

Length of output: 2885


🏁 Script executed:

rg -n "set_override|set_custom_assignment" --type=cpp -B3 -A5

Repository: absmartly/cpp-sdk

Length of output: 11180


🏁 Script executed:

find . -name "*test*.cpp" -o -name "*test*.h" | head -20

Repository: absmartly/cpp-sdk

Length of output: 276


🏁 Script executed:

rg -n "override|finalize" include/absmartly/context.h -B2 -A2

Repository: absmartly/cpp-sdk

Length of output: 1187


🏁 Script executed:

cat -n src/context.cpp | sed -n '700,710p'

Repository: absmartly/cpp-sdk

Length of output: 426


🏁 Script executed:

rg -n "should throw after finalize" tests/context_test.cpp -B5 -A10

Repository: absmartly/cpp-sdk

Length of output: 3843


set_override allows state mutation on finalised contexts — inconsistent with set_custom_assignment.

set_custom_assignment (line 164) calls check_not_finalized() to prevent mutations after finalisation, but set_override does not. The test suite (line 837–841) explicitly verifies that set_custom_assignment throws after finalisation, yet contains no corresponding test for set_override. If overrides are intentionally permitted post-finalisation, this should be documented; otherwise, add check_not_finalized() to set_override and set_overrides for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 153 - 155, set_override currently mutates
overrides_ after finalisation while set_custom_assignment enforces immutability
via check_not_finalized(); call check_not_finalized() at the start of both
Context::set_override and Context::set_overrides to prevent post-finalisation
mutation and keep behavior consistent with set_custom_assignment (or, if
overrides are intentionally mutable, add explicit documentation to that effect
instead). Ensure you reference the check_not_finalized() guard and the
overrides_ mutations in these methods so tests expecting post-finalisation
mutation to be disallowed will pass.

Comment on lines +273 to +278
if (field.type == "number") {
try {
return std::stod(val);
} catch (...) {
return nullptr;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

std::stod in custom_field_value has the same locale sensitivity issue as in evaluator.cpp.

For "number" type custom fields, std::stod is locale-dependent. This mirrors the issue flagged in evaluator.cpp. Consider using std::from_chars or a locale-independent parsing approach here as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 273 - 278, The code in custom_field_value uses
std::stod (in the block handling field.type == "number"), which is
locale-dependent; replace the std::stod call with a locale-independent parser
such as std::from_chars (include <charconv>), parse val into a double using
std::from_chars, check the returned error code (std::errc or
std::from_chars_result) and return nullptr on failure (same behavior as the
current catch path), and remove the try/catch; update the custom_field_value
function to handle the from_chars result and preserve the existing return
semantics.

Comment on lines +84 to +100
if (value.is_string()) {
const auto& str = value.get_ref<const std::string&>();
if (str.empty()) {
return std::nullopt;
}
try {
std::size_t pos = 0;
double result = std::stod(str, &pos);
if (pos == str.size() && std::isfinite(result)) {
return result;
}
} catch (...) {
}
return std::nullopt;
}
return std::nullopt;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

std::from_chars double support compiler versions C++17 GCC Clang MSVC requirements

💡 Result:

std::from_chars for double (floating-point overloads) — practical support matrix (C++17)

Toolchain Standard library Floating-point std::from_chars (incl. double)
GCC libstdc++ Implemented in libstdc++ trunk July 2020 (float/double/long double overloads were added then). (gcc.gnu.org) Later work switched float/double parsing to fast_float on IEEE binary32/64 targets. (gcc.gnu.org)
Clang libc++ libc++ reports: std::from_chars for float/double is available since libc++ v20; support is “complete except for long double”. (libcxx.llvm.org)
Clang libstdc++ Same as GCC’s libstdc++ version you’re linking against (so floating support depends on that libstdc++). (gcc.gnu.org)
MSVC MSVC STL Visual Studio 2017 15.8 implemented floating-point from_chars() in <charconv>. (devblogs.microsoft.com)

Notes / requirements

  • The floating overloads are the C++17 overloads in <charconv> taking a floating type and an optional std::chars_format. (en.cppreference.com)
  • If you need a portable compile-time check, be careful: the usual feature-test macro situation has historically been imperfect in some libstdc++ configurations (headers/macros vs actual availability). (gcc.gnu.org)

Citations:


🌐 Web query:

std::stod locale sensitivity decimal separator C locale example

💡 Result:

std::stod is locale-sensitive because it is specified in terms of the C library conversion (strtod), which uses the C locale’s LC_NUMERIC category to decide what the decimal separator is. So, if your process locale uses a comma decimal point (e.g., many European locales), "1,5" may parse as 1.5 and "1.5" may stop at the . (or fail depending on input handling). (stackoverflow.com)

Minimal example (showing the effect)

`#include` <clocale>
`#include` <iostream>
`#include` <string>

int main() {
  // Suppose your environment supports a comma-decimal locale like "de_DE.UTF-8".
  std::setlocale(LC_NUMERIC, "de_DE.UTF-8");
  std::cout << std::stod("1,5") << "\n";  // likely prints 1.5
  // std::cout << std::stod("1.5") << "\n"; // may parse only "1" then stop at '.'

  // Force "C" numeric rules: decimal point is '.'
  std::setlocale(LC_NUMERIC, "C");
  std::cout << std::stod("1.5") << "\n";  // prints 1.5
}

LC_NUMERIC is the locale part that affects strtod/stod, and you can guarantee “C-locale” parsing by setting LC_NUMERIC (or LC_ALL) to "C". (open-std.org)

If you need locale-independent parsing (recommended)

  • Prefer std::from_chars for floating-point (C++17/20; where implemented) since it’s not based on locales.
  • Or use a locale-fixed approach (e.g., a std::istringstream imbued with std::locale::classic()), instead of changing the global C locale (which is process-global and problematic in multi-threaded code). (stackoverflow.com)

Citations:


Locale-sensitive string-to-number parsing may produce incorrect results.

std::stod respects the current C locale. If the application (or a third-party library) sets a locale that uses , as the decimal separator (e.g., de_DE), parsing "1.5" will yield 1.0 (stopping at the dot) and the pos == str.size() guard will return std::nullopt — silently breaking numeric string coercion. For an SDK that must behave deterministically, prefer a locale-independent approach.

🔧 Proposed fix using std::from_chars

std::from_chars (C++17, locale-independent by specification):

-        try {
-            std::size_t pos = 0;
-            double result = std::stod(str, &pos);
-            if (pos == str.size() && std::isfinite(result)) {
-                return result;
-            }
-        } catch (...) {
-        }
+        double result = 0.0;
+        auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);
+        if (ec == std::errc{} && ptr == str.data() + str.size() && std::isfinite(result)) {
+            return result;
+        }

Compiler support: GCC 11+ (libstdc++), Clang 16+ (libc++), MSVC 19.15+ (Visual Studio 2017 15.8+).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (value.is_string()) {
const auto& str = value.get_ref<const std::string&>();
if (str.empty()) {
return std::nullopt;
}
try {
std::size_t pos = 0;
double result = std::stod(str, &pos);
if (pos == str.size() && std::isfinite(result)) {
return result;
}
} catch (...) {
}
return std::nullopt;
}
return std::nullopt;
}
if (value.is_string()) {
const auto& str = value.get_ref<const std::string&>();
if (str.empty()) {
return std::nullopt;
}
double result = 0.0;
auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);
if (ec == std::errc{} && ptr == str.data() + str.size() && std::isfinite(result)) {
return result;
}
return std::nullopt;
}
return std::nullopt;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/json_expr/evaluator.cpp` around lines 84 - 100, The string-to-double
conversion uses locale-sensitive std::stod; replace it with locale-independent
std::from_chars (include <charconv>) to parse the std::string 'str' (or a
std::string_view) into a double named 'result', remove the try/catch and the pos
variable, then check that from_chars returned no error and consumed the entire
input (the end pointer equals str.data()+str.size()) and that
std::isfinite(result) before returning result; otherwise return std::nullopt —
update the conversion logic in the block that currently references value, str,
pos, result to use std::from_chars instead.

Comment on lines +112 to +124
if (value.is_number()) {
double d = value.get<double>();
double int_part;
if (std::modf(d, &int_part) == 0.0 && std::abs(d) < 1e15) {
std::ostringstream oss;
oss << std::fixed << std::setprecision(0) << d;
return oss.str();
}
std::ostringstream oss;
oss << std::setprecision(15) << d;
std::string result = oss.str();
return result;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Locale-sensitive number-to-string formatting.

std::ostringstream uses the global locale by default. In locales with , as the decimal separator, to_string_value(1.5) would produce "1,5", breaking downstream string comparisons and audience matching. Imbue the stream with the classic "C" locale to ensure consistent output.

🔧 Proposed fix — imbue classic locale
     if (value.is_number()) {
         double d = value.get<double>();
         double int_part;
         if (std::modf(d, &int_part) == 0.0 && std::abs(d) < 1e15) {
             std::ostringstream oss;
+            oss.imbue(std::locale::classic());
             oss << std::fixed << std::setprecision(0) << d;
             return oss.str();
         }
         std::ostringstream oss;
+        oss.imbue(std::locale::classic());
         oss << std::setprecision(15) << d;
         std::string result = oss.str();
         return result;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/json_expr/evaluator.cpp` around lines 112 - 124, The number-to-string
path in src/json_expr/evaluator.cpp uses std::ostringstream (the two local
variables named oss) which pick up the global locale; imbue each ostringstream
with the C locale (call oss.imbue(std::locale::classic()) immediately after
construction) before formatting so decimal separators are always '.'; also
ensure <locale> is included if not already.

Comment on lines +169 to +174
for (; w < 16; ++w) {
block[w] = 0;
}

block[14] = static_cast<uint32_t>(len << 3);
md5cycle(state, block);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Encode the MD5 bit‑length as 64‑bit to avoid truncation.

On Line 173 the length is written as a 32‑bit value, so inputs ≥ 512 MB will hash incorrectly. MD5 appends a 64‑bit bit‑length; write both low and high words.

🔧 Suggested update
-    block[14] = static_cast<uint32_t>(len << 3);
-    md5cycle(state, block);
+    const uint64_t bit_len = static_cast<uint64_t>(len) << 3;
+    block[14] = static_cast<uint32_t>(bit_len & 0xFFFFFFFFu);
+    block[15] = static_cast<uint32_t>(bit_len >> 32);
+    md5cycle(state, block);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (; w < 16; ++w) {
block[w] = 0;
}
block[14] = static_cast<uint32_t>(len << 3);
md5cycle(state, block);
for (; w < 16; ++w) {
block[w] = 0;
}
const uint64_t bit_len = static_cast<uint64_t>(len) << 3;
block[14] = static_cast<uint32_t>(bit_len & 0xFFFFFFFFu);
block[15] = static_cast<uint32_t>(bit_len >> 32);
md5cycle(state, block);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/md5.cpp` around lines 169 - 174, The code currently writes only a 32-bit
length into block[14], truncating inputs ≥512MB; compute the full 64-bit
bit-length (e.g. uint64_t bitlen = static_cast<uint64_t>(len) << 3), then store
the low 32 bits into block[14] and the high 32 bits into block[15] before
calling md5cycle(state, block) so the MD5 length is encoded as a 64-bit value.

Add SDK::create() factory and sdk->create_context() / create_context_with()
convenience methods for simpler initialization.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (2)
README.md (2)

11-37: ⚠️ Potential issue | 🟡 Minor

Fix heading level jumps in the Installation section.

Lines 11, 28, and 37 jump from H2 to H4, which violates markdownlint's heading increment rule. These subsection headings should be H3.

📝 Proposed change
-#### CMake (FetchContent)
+### CMake (FetchContent)
 
 Add the following to your `CMakeLists.txt` to include the SDK directly from the repository:
 
@@ -25,7 +25,7 @@
 target_link_libraries(your_target PRIVATE absmartly-sdk)

-#### CMake (Local)
+### CMake (Local)

If you have cloned the repository locally, you can add it as a subdirectory:

@@ -34,7 +34,7 @@
target_link_libraries(your_target PRIVATE absmartly-sdk)


-#### Building from Source
+### Building from Source
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 11 - 37, Change the three subsection headings
currently using H4 (the lines titled "CMake (FetchContent)", "CMake (Local)",
and "Building from Source") to H3 so they consistently increment from the H2
"Installation" heading; update the heading markers from #### to ### for the
symbols "CMake (FetchContent)", "CMake (Local)", and "Building from Source" and
verify the surrounding blocks keep the same content and spacing.

238-238: ⚠️ Potential issue | 🟡 Minor

Add comma for improved readability in compound sentence.

The sentence connects two independent clauses and would read more smoothly with a comma before "and".

📝 Proposed change
-You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in and you want to associate the new identity with the context.
+You can add additional units to a context by calling the `set_unit()` or `set_units()` methods. This is useful when a user logs in, and you want to associate the new identity with the context.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 238, Insert a comma before the conjunction "and" in the
sentence "This is useful when a user logs in and you want to associate the new
identity with the context." so it reads "This is useful when a user logs in, and
you want to associate the new identity with the context." to improve
readability; update the README.md sentence accordingly.
🧹 Nitpick comments (7)
README.md (1)

21-21: Consider recommending a pinned version instead of "main".

Using GIT_TAG main in the FetchContent example will fetch the latest commit from the main branch, which could introduce breaking changes unexpectedly and makes builds non-reproducible. For production use, it's better to recommend a specific tag or commit SHA.

📌 Suggested alternative
 FetchContent_Declare(
   absmartly-sdk
   GIT_REPOSITORY https://github.com/absmartly/cpp-sdk.git
-  GIT_TAG main
+  GIT_TAG v1.0.0  # or a specific commit SHA
 )

Alternatively, add a note in the documentation explaining that users should replace main with a specific version tag for production use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 21, The README uses an unstable branch reference "GIT_TAG
main" which risks non-reproducible builds; update the documentation to recommend
pinning to a specific tag or commit (e.g., replace "GIT_TAG main" with a version
tag or commit SHA) and add a brief note advising users to use a fixed tag/sha
for production to ensure reproducible builds.
CMakeLists.txt (2)

4-5: Consider setting C++ standard per-target instead of globally.

Setting CMAKE_CXX_STANDARD globally affects all targets in the project. For better encapsulation, consider using target_compile_features(absmartly-sdk PUBLIC cxx_std_17) instead, which also ensures consumers use C++17.

♻️ Proposed refactor
-set(CMAKE_CXX_STANDARD 17)
-set(CMAKE_CXX_STANDARD_REQUIRED ON)
+# C++ standard set per-target below

Then after add_library:

 target_link_libraries(absmartly-sdk PUBLIC nlohmann_json::nlohmann_json)
+target_compile_features(absmartly-sdk PUBLIC cxx_std_17)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CMakeLists.txt` around lines 4 - 5, The CMake file currently sets C++17
globally via CMAKE_CXX_STANDARD; instead, remove or stop relying on the global
setting and set the standard per-target by adding
target_compile_features(absmartly-sdk PUBLIC cxx_std_17) immediately after the
add_library(...) that defines the absmartly-sdk target, so the absmartly-sdk
target and its consumers are required to use C++17 without impacting other
targets.

47-47: Consider defaulting ABSMARTLY_BUILD_TESTS to OFF.

Having tests enabled by default means consumers who use add_subdirectory or FetchContent will build tests unnecessarily. A common convention is to default tests to OFF for library projects, or enable them only when built as the top-level project.

♻️ Proposed refactor
-option(ABSMARTLY_BUILD_TESTS "Build tests" ON)
+option(ABSMARTLY_BUILD_TESTS "Build tests" OFF)
+
+# Or conditionally enable when this is the main project:
+# if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
+#     set(ABSMARTLY_BUILD_TESTS_DEFAULT ON)
+# else()
+#     set(ABSMARTLY_BUILD_TESTS_DEFAULT OFF)
+# endif()
+# option(ABSMARTLY_BUILD_TESTS "Build tests" ${ABSMARTLY_BUILD_TESTS_DEFAULT})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CMakeLists.txt` at line 47, Change the option ABSMARTLY_BUILD_TESTS to
default OFF instead of ON; update the CMakeLists to declare
option(ABSMARTLY_BUILD_TESTS "Build tests" OFF) and, if you want to keep the old
top-level behavior, wrap a top-level check such as comparing CMAKE_SOURCE_DIR
and CMAKE_CURRENT_SOURCE_DIR to set it to ON only when the project is the
top-level build (or conditionally add tests only when that variable is true).
Ensure references to ABSMARTLY_BUILD_TESTS in add_subdirectory/test-target logic
remain unchanged.
include/absmartly/context.h (2)

41-44: Document ownership semantics for raw pointer in ExperimentIndex.

ExperimentIndex::data is a raw pointer to ExperimentData. The ownership relationship should be documented—presumably this points into data_.experiments and is only valid whilst the parent Context is alive. Consider adding a comment to clarify this invariant.

📝 Suggested documentation
 struct ExperimentIndex {
-    const ExperimentData* data = nullptr;
+    const ExperimentData* data = nullptr;  // Non-owning; points into Context::data_.experiments
     std::vector<std::map<std::string, nlohmann::json>> variables;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/absmartly/context.h` around lines 41 - 44, Document that
ExperimentIndex::data is a non-owning pointer: add a concise comment on the
ExperimentIndex struct stating that data is a raw, non-owning pointer to an
ExperimentData that lives inside the parent Context (e.g.,
Context::data_.experiments) and is only valid while that Context remains alive;
mention the invariant that ExperimentIndex instances must not outlive their
owning Context and that no deletion is performed through this pointer.

12-12: <set> header appears unused in this header file.

The <set> include is not used in any declarations here. If it is only needed in the implementation (src/context.cpp), move it there to reduce header dependencies and compilation times.

♻️ Proposed fix
 `#include` <map>
-#include <set>
 `#include` <vector>

Then add #include <set> in src/context.cpp if needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/absmartly/context.h` at line 12, Remove the unused `#include` <set>
from the context.h header and, if any std::set usage exists in the
implementation of the context declarations, add `#include` <set> to the
corresponding source file that implements those functions (the context.cpp
implementation). This reduces header dependencies and keeps the header minimal.
src/context.cpp (2)

14-20: Floating-point exact equality comparison may be fragile.

array_equals_shallow uses == to compare double values. While this may be intentional for comparing stored traffic splits that originated from the same source, floating-point exact equality can be problematic if values are ever computed differently. Consider whether an epsilon-based comparison is warranted, or add a comment explaining why exact equality is safe here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 14 - 20, The function array_equals_shallow uses
exact equality on doubles which is brittle; update array_equals_shallow to
perform comparisons with a small relative/absolute epsilon (or use
std::fabs(a[i]-b[i]) <= epsilon) to tolerate floating-point rounding, and
choose/define a sensible EPSILON constant nearby (or accept an optional
tolerance parameter); alternatively, if exact bitwise equality is intended, add
a clear comment in array_equals_shallow explaining why exact equality is safe
and under what guarantees (e.g., values always come from the same source) so
future maintainers won’t change it.

56-59: Redundant assignment of data_ in init().

become_ready() already moves data into data_ at line 57, then calls init(data_) which assigns data_ again at line 618. The second assignment is redundant.

♻️ Proposed fix
 void Context::init(const ContextData& data) {
-    data_ = data;
     build_index();
 }

Or alternatively, if init is intended to be callable independently:

 void Context::become_ready(ContextData data) {
-    data_ = std::move(data);
-    init(data_);
+    init(std::move(data));
     ready_ = true;
     // ...
 }
 
-void Context::init(const ContextData& data) {
-    data_ = data;
+void Context::init(ContextData data) {
+    data_ = std::move(data);
     build_index();
 }

Also applies to: 617-620

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 56 - 59, Context::become_ready currently moves
the incoming ContextData into data_ then calls init(data_), but init also
reassigns data_ (redundant); remove the duplicate assignment inside
Context::init (the data_ = std::move(data) or similar) and have init operate on
the already-stored data_ (or change init to take no parameter), leaving
Context::become_ready as data_ = std::move(data); init(); ready_ = true; so the
double move/assignment is eliminated; update references to the parameter inside
Context::init accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@include/absmartly/sdk.h`:
- Around line 1-7: Add the missing <future> include so std::future is available:
update the header (include/absmartly/sdk.h) to `#include` <future> near the other
includes, ensuring the declaration std::future<ContextData> get_context_data()
compiles; keep the include order consistent with existing standard headers and
avoid adding other code changes.
- Line 3: The SDK references an undefined type SDKConfig and includes a missing
header; create a new header that defines the SDKConfig type (e.g., a simple
struct with the configuration fields used by class SDK and any defaults),
protect it with include guards or `#pragma` once, and ensure it declares anything
the SDK methods (constructor/factory methods) expect; then keep the existing
`#include` <absmartly/sdk_config.h> in the SDK header so SDK can compile. If you
prefer not to add a header, remove the `#include` and replace SDKConfig usages
inside class SDK with a suitable existing type or a forward declaration and
update the SDK methods (e.g., SDK::create, SDK constructor) accordingly.

In `@README.md`:
- Around line 455-469: The README links for each SDK (e.g., link items like
"[Java SDK]", "[JavaScript SDK]", "[C++ SDK]" etc.) use non-canonical URLs with
the "www" subdomain (https://www.github.com/...). Update each SDK link target to
use the canonical GitHub domain by removing "www" so they read
https://github.com/absmartly/<repo>, ensuring every listed link (Java,
JavaScript, PHP, Swift, Vue2, Vue3, React, Angular, Python3, Go, Ruby, .NET,
C++, Dart, Flutter) is changed accordingly.

In `@src/context.cpp`:
- Around line 70-88: The wait_until_ready() implementation calls
data_future_.get() which is single-use; change data_future_ from std::future<T>
to std::shared_future<T> (update its declaration and wherever it is
assigned/constructed) so multiple calls to Context::wait_until_ready() are safe,
then replace .get() usage in wait_until_ready() with the shared_future's .get();
ensure constructors or factory functions that produce the future convert or call
.share() to initialize data_future_, and keep existing logic around ready_,
failed_, become_ready, and emit_event unchanged.
- Around line 644-645: The silent catch(...) in src/context.cpp that wraps
parsing of variant.config should be replaced with a typed catch that logs the
error and the offending config so parse failures are discoverable: catch
std::exception (e.g., catch (const std::exception& e)) and call your existing
logging/emitter (e.g., processLogger.warn or emitEvent) including e.what() and
the raw variant.config (and variant identifier if available); if non-std
exceptions must be handled too, add a final catch(...) that logs a generic parse
failure. Ensure the log message clearly names the operation (parsing
variant.config) and includes the value and error details.
- Around line 382-418: The Context class (methods like publish(), and members
pending_, exposures_, goals_, attrs_, units_) is not thread-safe; either add
synchronization or document single-threaded usage: to fix, add a std::mutex
member to Context (e.g., mutex_), protect all accesses and mutations of
pending_, exposures_, goals_, attrs_, and units_ by locking (use
std::lock_guard<std::mutex> or std::scoped_lock) in publish() and any other
methods that read/write these members, or alternatively add a clear comment in
the class header and README stating Context must only be used from a single
thread and ensure all call sites are single-threaded.

---

Duplicate comments:
In `@README.md`:
- Around line 11-37: Change the three subsection headings currently using H4
(the lines titled "CMake (FetchContent)", "CMake (Local)", and "Building from
Source") to H3 so they consistently increment from the H2 "Installation"
heading; update the heading markers from #### to ### for the symbols "CMake
(FetchContent)", "CMake (Local)", and "Building from Source" and verify the
surrounding blocks keep the same content and spacing.
- Line 238: Insert a comma before the conjunction "and" in the sentence "This is
useful when a user logs in and you want to associate the new identity with the
context." so it reads "This is useful when a user logs in, and you want to
associate the new identity with the context." to improve readability; update the
README.md sentence accordingly.

---

Nitpick comments:
In `@CMakeLists.txt`:
- Around line 4-5: The CMake file currently sets C++17 globally via
CMAKE_CXX_STANDARD; instead, remove or stop relying on the global setting and
set the standard per-target by adding target_compile_features(absmartly-sdk
PUBLIC cxx_std_17) immediately after the add_library(...) that defines the
absmartly-sdk target, so the absmartly-sdk target and its consumers are required
to use C++17 without impacting other targets.
- Line 47: Change the option ABSMARTLY_BUILD_TESTS to default OFF instead of ON;
update the CMakeLists to declare option(ABSMARTLY_BUILD_TESTS "Build tests" OFF)
and, if you want to keep the old top-level behavior, wrap a top-level check such
as comparing CMAKE_SOURCE_DIR and CMAKE_CURRENT_SOURCE_DIR to set it to ON only
when the project is the top-level build (or conditionally add tests only when
that variable is true). Ensure references to ABSMARTLY_BUILD_TESTS in
add_subdirectory/test-target logic remain unchanged.

In `@include/absmartly/context.h`:
- Around line 41-44: Document that ExperimentIndex::data is a non-owning
pointer: add a concise comment on the ExperimentIndex struct stating that data
is a raw, non-owning pointer to an ExperimentData that lives inside the parent
Context (e.g., Context::data_.experiments) and is only valid while that Context
remains alive; mention the invariant that ExperimentIndex instances must not
outlive their owning Context and that no deletion is performed through this
pointer.
- Line 12: Remove the unused `#include` <set> from the context.h header and, if
any std::set usage exists in the implementation of the context declarations, add
`#include` <set> to the corresponding source file that implements those functions
(the context.cpp implementation). This reduces header dependencies and keeps the
header minimal.

In `@README.md`:
- Line 21: The README uses an unstable branch reference "GIT_TAG main" which
risks non-reproducible builds; update the documentation to recommend pinning to
a specific tag or commit (e.g., replace "GIT_TAG main" with a version tag or
commit SHA) and add a brief note advising users to use a fixed tag/sha for
production to ensure reproducible builds.

In `@src/context.cpp`:
- Around line 14-20: The function array_equals_shallow uses exact equality on
doubles which is brittle; update array_equals_shallow to perform comparisons
with a small relative/absolute epsilon (or use std::fabs(a[i]-b[i]) <= epsilon)
to tolerate floating-point rounding, and choose/define a sensible EPSILON
constant nearby (or accept an optional tolerance parameter); alternatively, if
exact bitwise equality is intended, add a clear comment in array_equals_shallow
explaining why exact equality is safe and under what guarantees (e.g., values
always come from the same source) so future maintainers won’t change it.
- Around line 56-59: Context::become_ready currently moves the incoming
ContextData into data_ then calls init(data_), but init also reassigns data_
(redundant); remove the duplicate assignment inside Context::init (the data_ =
std::move(data) or similar) and have init operate on the already-stored data_
(or change init to take no parameter), leaving Context::become_ready as data_ =
std::move(data); init(); ready_ = true; so the double move/assignment is
eliminated; update references to the parameter inside Context::init accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b83f1e4 and 6f7ffd3.

📒 Files selected for processing (5)
  • CMakeLists.txt
  • README.md
  • include/absmartly/context.h
  • include/absmartly/sdk.h
  • src/context.cpp

Comment on lines +1 to +7
#pragma once

#include <absmartly/sdk_config.h>
#include <absmartly/context.h>
#include <absmartly/context_config.h>
#include <memory>

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing <future> header for std::future usage.

Line 17 declares std::future<ContextData> get_context_data() but <future> is not included. This will cause a compilation error.

🐛 Proposed fix
 `#include` <absmartly/sdk_config.h>
 `#include` <absmartly/context.h>
 `#include` <absmartly/context_config.h>
 `#include` <memory>
+#include <future>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#pragma once
#include <absmartly/sdk_config.h>
#include <absmartly/context.h>
#include <absmartly/context_config.h>
#include <memory>
`#pragma` once
`#include` <absmartly/sdk_config.h>
`#include` <absmartly/context.h>
`#include` <absmartly/context_config.h>
`#include` <memory>
`#include` <future>
🧰 Tools
🪛 Clang (14.0.6)

[error] 3-3: 'absmartly/sdk_config.h' file not found

(clang-diagnostic-error)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/absmartly/sdk.h` around lines 1 - 7, Add the missing <future> include
so std::future is available: update the header (include/absmartly/sdk.h) to
`#include` <future> near the other includes, ensuring the declaration
std::future<ContextData> get_context_data() compiles; keep the include order
consistent with existing standard headers and avoid adding other code changes.

@@ -0,0 +1,28 @@
#pragma once

#include <absmartly/sdk_config.h>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for sdk_config.h anywhere in the repository
echo "=== Searching for sdk_config.h in the entire repository ==="
fd -t f "sdk_config.h" .

echo ""
echo "=== Checking include/absmartly/ directory structure ==="
fd -t f . include/absmartly/ | head -20

Repository: absmartly/cpp-sdk

Length of output: 542


🏁 Script executed:

#!/bin/bash
# Check the content of sdk.h to see the context around the include
cat -n include/absmartly/sdk.h | head -30

Repository: absmartly/cpp-sdk

Length of output: 1027


🏁 Script executed:

#!/bin/bash
# Search for SDKConfig definition in the codebase
echo "=== Searching for SDKConfig definition ==="
rg -n "class SDKConfig|struct SDKConfig" include/

echo ""
echo "=== Searching for SDKConfig usage ==="
rg -n "SDKConfig" include/ | head -20

Repository: absmartly/cpp-sdk

Length of output: 290


🏁 Script executed:

#!/bin/bash
# Search for SDKConfig definition in the entire repository, including src/
echo "=== Searching for SDKConfig definition in entire repository ==="
rg -n "class SDKConfig|struct SDKConfig" .

echo ""
echo "=== Searching for sdk_config files (any variant) ==="
fd -t f "*sdk_config*" .

Repository: absmartly/cpp-sdk

Length of output: 487


🏁 Script executed:

#!/bin/bash
# Search for SDKConfig definition in entire repository with correct syntax
echo "=== Searching for SDKConfig definition in entire repository ==="
rg -n "class SDKConfig|struct SDKConfig" . --type cpp --type h

echo ""
echo "=== Searching for sdk_config files ==="
fd -t f --fixed-strings "sdk_config" .

echo ""
echo "=== Listing all header files in include/absmartly/ ==="
ls -la include/absmartly/

Repository: absmartly/cpp-sdk

Length of output: 955


🏁 Script executed:

#!/bin/bash
# Check context_config.h to see if there's a clue about SDKConfig
echo "=== Content of context_config.h ==="
cat -n include/absmartly/context_config.h

echo ""
echo "=== Searching for SDKConfig in source files ==="
fd -t f . src/ 2>/dev/null | head -20

echo ""
echo "=== Check if src directory exists ==="
ls -la src/ 2>/dev/null || echo "src/ directory not found"

Repository: absmartly/cpp-sdk

Length of output: 1328


🏁 Script executed:

#!/bin/bash
# Search for SDKConfig definition in source files
echo "=== Searching for SDKConfig in all files ==="
rg -n "SDKConfig" . --type cpp --type h -A 3 -B 1

echo ""
echo "=== Check for any .hpp files ==="
fd -t f --fixed-strings ".hpp" include/

Repository: absmartly/cpp-sdk

Length of output: 818


Create sdk_config.h or remove the missing include.

The header file include/absmartly/sdk_config.h does not exist in the repository. The SDK class declares methods using the undefined SDKConfig type (lines 12, 20), which will cause compilation failure. Either the header file must be created with the SDKConfig struct definition, or the include and type references must be removed or corrected.

🧰 Tools
🪛 Clang (14.0.6)

[error] 3-3: 'absmartly/sdk_config.h' file not found

(clang-diagnostic-error)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@include/absmartly/sdk.h` at line 3, The SDK references an undefined type
SDKConfig and includes a missing header; create a new header that defines the
SDKConfig type (e.g., a simple struct with the configuration fields used by
class SDK and any defaults), protect it with include guards or `#pragma` once, and
ensure it declares anything the SDK methods (constructor/factory methods)
expect; then keep the existing `#include` <absmartly/sdk_config.h> in the SDK
header so SDK can compile. If you prefer not to add a header, remove the
`#include` and replace SDKConfig usages inside class SDK with a suitable existing
type or a forward declaration and update the SDK methods (e.g., SDK::create, SDK
constructor) accordingly.

Comment on lines +455 to +469
- [Java SDK](https://www.github.com/absmartly/java-sdk)
- [JavaScript SDK](https://www.github.com/absmartly/javascript-sdk)
- [PHP SDK](https://www.github.com/absmartly/php-sdk)
- [Swift SDK](https://www.github.com/absmartly/swift-sdk)
- [Vue2 SDK](https://www.github.com/absmartly/vue2-sdk)
- [Vue3 SDK](https://www.github.com/absmartly/vue3-sdk)
- [React SDK](https://www.github.com/absmartly/react-sdk)
- [Angular SDK](https://www.github.com/absmartly/angular-sdk)
- [Python3 SDK](https://www.github.com/absmartly/python3-sdk)
- [Go SDK](https://www.github.com/absmartly/go-sdk)
- [Ruby SDK](https://www.github.com/absmartly/ruby-sdk)
- [.NET SDK](https://www.github.com/absmartly/dotnet-sdk)
- [C++ SDK](https://www.github.com/absmartly/cpp-sdk) (this package)
- [Dart SDK](https://www.github.com/absmartly/dart-sdk)
- [Flutter SDK](https://www.github.com/absmartly/flutter-sdk)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use canonical GitHub URLs without "www" subdomain.

All SDK links include the "www" subdomain (https://www.github.com/...), but GitHub's canonical domain is github.com without "www". Whilst the links work due to redirects, it's better to use the canonical form.

🔗 Proposed change
-### Have a look at our growing list of clients and SDKs:
-- [Java SDK](https://www.github.com/absmartly/java-sdk)
-- [JavaScript SDK](https://www.github.com/absmartly/javascript-sdk)
-- [PHP SDK](https://www.github.com/absmartly/php-sdk)
-- [Swift SDK](https://www.github.com/absmartly/swift-sdk)
-- [Vue2 SDK](https://www.github.com/absmartly/vue2-sdk)
-- [Vue3 SDK](https://www.github.com/absmartly/vue3-sdk)
-- [React SDK](https://www.github.com/absmartly/react-sdk)
-- [Angular SDK](https://www.github.com/absmartly/angular-sdk)
-- [Python3 SDK](https://www.github.com/absmartly/python3-sdk)
-- [Go SDK](https://www.github.com/absmartly/go-sdk)
-- [Ruby SDK](https://www.github.com/absmartly/ruby-sdk)
-- [.NET SDK](https://www.github.com/absmartly/dotnet-sdk)
-- [C++ SDK](https://www.github.com/absmartly/cpp-sdk) (this package)
-- [Dart SDK](https://www.github.com/absmartly/dart-sdk)
-- [Flutter SDK](https://www.github.com/absmartly/flutter-sdk)
+### Have a look at our growing list of clients and SDKs:
+- [Java SDK](https://github.com/absmartly/java-sdk)
+- [JavaScript SDK](https://github.com/absmartly/javascript-sdk)
+- [PHP SDK](https://github.com/absmartly/php-sdk)
+- [Swift SDK](https://github.com/absmartly/swift-sdk)
+- [Vue2 SDK](https://github.com/absmartly/vue2-sdk)
+- [Vue3 SDK](https://github.com/absmartly/vue3-sdk)
+- [React SDK](https://github.com/absmartly/react-sdk)
+- [Angular SDK](https://github.com/absmartly/angular-sdk)
+- [Python3 SDK](https://github.com/absmartly/python3-sdk)
+- [Go SDK](https://github.com/absmartly/go-sdk)
+- [Ruby SDK](https://github.com/absmartly/ruby-sdk)
+- [.NET SDK](https://github.com/absmartly/dotnet-sdk)
+- [C++ SDK](https://github.com/absmartly/cpp-sdk) (this package)
+- [Dart SDK](https://github.com/absmartly/dart-sdk)
+- [Flutter SDK](https://github.com/absmartly/flutter-sdk)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- [Java SDK](https://www.github.com/absmartly/java-sdk)
- [JavaScript SDK](https://www.github.com/absmartly/javascript-sdk)
- [PHP SDK](https://www.github.com/absmartly/php-sdk)
- [Swift SDK](https://www.github.com/absmartly/swift-sdk)
- [Vue2 SDK](https://www.github.com/absmartly/vue2-sdk)
- [Vue3 SDK](https://www.github.com/absmartly/vue3-sdk)
- [React SDK](https://www.github.com/absmartly/react-sdk)
- [Angular SDK](https://www.github.com/absmartly/angular-sdk)
- [Python3 SDK](https://www.github.com/absmartly/python3-sdk)
- [Go SDK](https://www.github.com/absmartly/go-sdk)
- [Ruby SDK](https://www.github.com/absmartly/ruby-sdk)
- [.NET SDK](https://www.github.com/absmartly/dotnet-sdk)
- [C++ SDK](https://www.github.com/absmartly/cpp-sdk) (this package)
- [Dart SDK](https://www.github.com/absmartly/dart-sdk)
- [Flutter SDK](https://www.github.com/absmartly/flutter-sdk)
### Have a look at our growing list of clients and SDKs:
- [Java SDK](https://github.com/absmartly/java-sdk)
- [JavaScript SDK](https://github.com/absmartly/javascript-sdk)
- [PHP SDK](https://github.com/absmartly/php-sdk)
- [Swift SDK](https://github.com/absmartly/swift-sdk)
- [Vue2 SDK](https://github.com/absmartly/vue2-sdk)
- [Vue3 SDK](https://github.com/absmartly/vue3-sdk)
- [React SDK](https://github.com/absmartly/react-sdk)
- [Angular SDK](https://github.com/absmartly/angular-sdk)
- [Python3 SDK](https://github.com/absmartly/python3-sdk)
- [Go SDK](https://github.com/absmartly/go-sdk)
- [Ruby SDK](https://github.com/absmartly/ruby-sdk)
- [.NET SDK](https://github.com/absmartly/dotnet-sdk)
- [C++ SDK](https://github.com/absmartly/cpp-sdk) (this package)
- [Dart SDK](https://github.com/absmartly/dart-sdk)
- [Flutter SDK](https://github.com/absmartly/flutter-sdk)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 455 - 469, The README links for each SDK (e.g., link
items like "[Java SDK]", "[JavaScript SDK]", "[C++ SDK]" etc.) use non-canonical
URLs with the "www" subdomain (https://www.github.com/...). Update each SDK link
target to use the canonical GitHub domain by removing "www" so they read
https://github.com/absmartly/<repo>, ensuring every listed link (Java,
JavaScript, PHP, Swift, Vue2, Vue3, React, Angular, Python3, Go, Ruby, .NET,
C++, Dart, Flutter) is changed accordingly.

Comment on lines +70 to +88
void Context::wait_until_ready() {
if (ready_ || failed_) {
return;
}

if (!data_future_.valid()) {
failed_ = true;
emit_event("error", {{"message", "No data future available"}});
return;
}

try {
auto data = data_future_.get();
become_ready(std::move(data));
} catch (const std::exception& e) {
failed_ = true;
emit_event("error", {{"message", e.what()}});
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

wait_until_ready() can only be called once due to std::future::get() semantics.

std::future::get() can only be called once; subsequent calls result in undefined behaviour or an exception (depending on implementation). If wait_until_ready() is called twice before the future resolves, the second call will fail. Consider using std::shared_future if multiple waits are expected, or document that this method must only be called once.

♻️ Alternative using shared_future

In header:

-    std::future<ContextData> data_future_;
+    std::shared_future<ContextData> data_future_;

In constructor:

-    , data_future_(std::move(data_future)) {
+    , data_future_(data_future.share()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 70 - 88, The wait_until_ready() implementation
calls data_future_.get() which is single-use; change data_future_ from
std::future<T> to std::shared_future<T> (update its declaration and wherever it
is assigned/constructed) so multiple calls to Context::wait_until_ready() are
safe, then replace .get() usage in wait_until_ready() with the shared_future's
.get(); ensure constructors or factory functions that produce the future convert
or call .share() to initialize data_future_, and keep existing logic around
ready_, failed_, become_ready, and emit_event unchanged.

Comment on lines +382 to +418
PublishEvent Context::publish() {
if (pending_ == 0) {
return {};
}

PublishEvent event;
event.hashed = true;
event.publishedAt = now_millis();

for (const auto& [type, uid] : units_) {
Unit u;
u.type = type;
u.uid = unit_hash(type);
event.units.push_back(std::move(u));
}

if (!exposures_.empty()) {
event.exposures = std::move(exposures_);
exposures_.clear();
}

if (!goals_.empty()) {
event.goals = std::move(goals_);
goals_.clear();
}

if (!attrs_.empty()) {
event.attributes = attrs_;
}

pending_ = 0;

nlohmann::json pub_data = event;
emit_event("publish", pub_data);

return event;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the Context class definition
fd -t f -e h context.h | head -5

Repository: absmartly/cpp-sdk

Length of output: 88


🏁 Script executed:

# Search for thread-related documentation and synchronisation keywords
rg -i "thread|mutex|lock|atomic" include/absmartly/ --type cpp --type h

Repository: absmartly/cpp-sdk

Length of output: 43


🏁 Script executed:

# Examine the full context.h file to understand the class structure
cat include/absmartly/context.h

Repository: absmartly/cpp-sdk

Length of output: 5254


🏁 Script executed:

# Look at the context.cpp file around the publish() method and check for any synchronisation
sed -n '1,50p' src/context.cpp

Repository: absmartly/cpp-sdk

Length of output: 1650


🏁 Script executed:

# Check if there's a README or documentation about thread-safety
find . -maxdepth 2 -name "README*" -o -name "*.md" | head -10

Repository: absmartly/cpp-sdk

Length of output: 72


🏁 Script executed:

# Read the README to check for thread-safety documentation
cat README.md

Repository: absmartly/cpp-sdk

Length of output: 16471


🏁 Script executed:

# Search more broadly in the entire src/context.cpp file for synchronisation
rg "mutex|lock|atomic|thread" src/context.cpp

Repository: absmartly/cpp-sdk

Length of output: 109


🏁 Script executed:

# Check how Context is constructed and used in tests/examples
fd -t f -e cpp -e h test examples | head -20

Repository: absmartly/cpp-sdk

Length of output: 156


🏁 Script executed:

# Search for any documentation comments in context.cpp about thread-safety
rg -B5 -A5 "publish\(" src/context.cpp | head -40

Repository: absmartly/cpp-sdk

Length of output: 406


🏁 Script executed:

# Check if there are any member variables that suggest thread-safety (like mutex)
rg "std::mutex|std::lock|std::atomic" include/absmartly/

Repository: absmartly/cpp-sdk

Length of output: 43


publish() (and the entire Context class) is not thread-safe.

publish() and other methods read and modify shared state (pending_, exposures_, goals_, attrs_, units_) without synchronisation. The class contains no synchronisation primitives (no std::mutex, std::atomic, or locks). Data races will occur if Context is accessed concurrently from multiple threads. Either:

  • Add thread-safe synchronisation if multi-threaded use is intended, or
  • Document prominently that Context is not thread-safe and must be used from a single thread only.

The README provides no guidance on this constraint, and the usage examples suggest single-threaded usage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 382 - 418, The Context class (methods like
publish(), and members pending_, exposures_, goals_, attrs_, units_) is not
thread-safe; either add synchronization or document single-threaded usage: to
fix, add a std::mutex member to Context (e.g., mutex_), protect all accesses and
mutations of pending_, exposures_, goals_, attrs_, and units_ by locking (use
std::lock_guard<std::mutex> or std::scoped_lock) in publish() and any other
methods that read/write these members, or alternatively add a clear comment in
the class header and README stating Context must only be used from a single
thread and ensure all call sites are single-threaded.

Comment on lines +644 to +645
} catch (...) {
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent catch block hides JSON parse errors.

If variant.config contains invalid JSON, the error is silently ignored. Consider logging a warning or emitting an event so configuration issues are discoverable during debugging.

🛠️ Suggested improvement
                     try {
                         auto config_json = nlohmann::json::parse(config_str);
                         if (config_json.is_object()) {
                             for (auto it = config_json.begin(); it != config_json.end(); ++it) {
                                 parsed[it.key()] = it.value();
                             }
                         }
-                    } catch (...) {
+                    } catch (const std::exception& e) {
+                        // Optionally log or emit an event about malformed config
+                        // e.g., emit_event("warning", {{"message", "Failed to parse variant config"}, {"error", e.what()}});
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context.cpp` around lines 644 - 645, The silent catch(...) in
src/context.cpp that wraps parsing of variant.config should be replaced with a
typed catch that logs the error and the offending config so parse failures are
discoverable: catch std::exception (e.g., catch (const std::exception& e)) and
call your existing logging/emitter (e.g., processLogger.warn or emitEvent)
including e.what() and the raw variant.config (and variant identifier if
available); if non-std exceptions must be handled too, add a final catch(...)
that logs a generic parse failure. Ensure the log message clearly names the
operation (parsing variant.config) and includes the value and error details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant