Skip to content

Commit

Permalink
Feat: support anon id for segment (#2934)
Browse files Browse the repository at this point in the history
* ffi with anon_id

* anon id on rust side

* compile dll

* encapsulate anon_id

* adjusted flow to be sync with anonymusId

* small clean-up

---------

Co-authored-by: Vitaly Popuzin <[email protected]>
  • Loading branch information
NickKhalow and popuz authored Dec 12, 2024
1 parent eb04fb6 commit 59dc42c
Show file tree
Hide file tree
Showing 15 changed files with 115 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ public class AnalyticsController : IAnalyticsController
{
private readonly IAnalyticsService analytics;

private bool isInitialized;
public AnalyticsConfiguration Configuration { get; }

public AnalyticsController(
Expand All @@ -31,11 +30,10 @@ public AnalyticsController(

public void Initialize(IWeb3Identity? web3Identity)
{
analytics.Identify(web3Identity?.Address ?? "not cached");
isInitialized = true;
if (web3Identity != null && web3Identity.Address != null)
analytics.Identify(web3Identity?.Address);

TrackSystemInfo();

analytics.Flush();
}

Expand All @@ -49,9 +47,6 @@ public void SetCommonParam(IRealmData realmData, IWeb3IdentityCache? identityCac

public void Track(string eventName, JsonObject? properties = null)
{
if (!isInitialized)
ReportHub.LogError(ReportCategory.ANALYTICS, $"Analytics {nameof(Track)} called before initialization. Event {eventName} won't be tracked.");

if (Configuration.EventIsEnabled(eventName))
analytics.Track(eventName, properties);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public SegmentAnalyticsService(Configuration segmentConfiguration)
analytics.Flush();
}

public void Identify(string userId, JsonObject? traits = null) =>
analytics.Identify(userId, traits!);
public void Identify(string? userId, JsonObject? traits = null) =>
analytics.Identify(userId!, traits!);

public void Track(string eventName, JsonObject? properties = null) =>
analytics.Track(eventName, properties!);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public CountFlushAnalyticsServiceDecorator(IAnalyticsService origin, int flushCo
this.flushCount = flushCount;
}

public void Identify(string userId, JsonObject? traits = null)
public void Identify(string? userId, JsonObject? traits = null)
{
lock (monitor)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class DebugAnalyticsService : IAnalyticsService
{
private static readonly IEnumerable<KeyValuePair<string, JsonElement>> EMPTY = new Dictionary<string, JsonElement>();

public void Identify(string userId, JsonObject? traits = null)
public void Identify(string? userId, JsonObject? traits = null)
{
ReportHub.Log(ReportCategory.ANALYTICS, $"Identify: userId = {userId} | traits = {traits}");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace DCL.PerformanceAndDiagnostics.Analytics
/// </summary>
public interface IAnalyticsService
{
void Identify(string userId, JsonObject? traits = null);
void Identify(string? userId, JsonObject? traits = null);

/// <summary>
/// To track an event you have to call identify first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public TimeFlushAnalyticsServiceDecorator(IAnalyticsService origin, TimeSpan flu
FlushLoopAsync(token).Forget();
}

public void Identify(string userId, JsonObject? traits = null)
public void Identify(string? userId, JsonObject? traits = null)
{
origin.Identify(userId, traits);
}
Expand Down
Binary file not shown.
Git LFS file not shown
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ namespace Plugins.RustSegment.SegmentServerWrap
{
public readonly struct MarshaledString : IDisposable
{
/// <summary>
/// Ptr can be NULL
/// </summary>
public readonly IntPtr Ptr;

public MarshaledString(string str)
public MarshaledString(string? str)
{
Ptr = Marshal.StringToHGlobalAnsi(str);
Ptr = str == null ? IntPtr.Zero : Marshal.StringToHGlobalAnsi(str);
}

public void Dispose() =>
Marshal.FreeHGlobal(Ptr);
public void Dispose()
{
if (Ptr != IntPtr.Zero)
Marshal.FreeHGlobal(Ptr);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ SegmentFfiCallback callback
[DllImport(LIBRARY_NAME, CallingConvention = CALLING_CONVENTION, CharSet = CHAR_SET, EntryPoint = "segment_server_identify")]
internal static extern ulong SegmentServerIdentify(
IntPtr usedId,
IntPtr anonId,
IntPtr traitsJson,
IntPtr contextJson
);

[DllImport(LIBRARY_NAME, CallingConvention = CALLING_CONVENTION, CharSet = CHAR_SET, EntryPoint = "segment_server_track")]
internal static extern ulong SegmentServerTrack(
IntPtr usedId,
IntPtr anonId,
IntPtr eventName,
IntPtr propertiesJson,
IntPtr contextJson
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Segment.Serialization;
using System;
using System.Collections.Generic;
using UnityEngine.Device;
using UnityEngine.Pool;

namespace Plugins.RustSegment.SegmentServerWrap
Expand All @@ -23,9 +24,13 @@ private enum Operation
}

private const string EMPTY_JSON = "{}";
private volatile string cachedUserId = string.Empty;

private readonly string anonId;
private volatile string? cachedUserId;

private readonly Dictionary<ulong, (Operation, List<MarshaledString>)> afterClean = new ();
private readonly IContextSource contextSource = new ContextSource();

private static volatile RustSegmentAnalyticsService? current;

public RustSegmentAnalyticsService(string writerKey)
Expand All @@ -36,6 +41,8 @@ public RustSegmentAnalyticsService(string writerKey)
if (current != null)
throw new Exception("Rust Segment previous instance is not disposed");

this.anonId = SystemInfo.deviceUniqueIdentifier!;

using var mWriterKey = new MarshaledString(writerKey);
bool result = NativeMethods.SegmentServerInitialize(mWriterKey.Ptr, Callback);

Expand All @@ -55,19 +62,20 @@ public RustSegmentAnalyticsService(string writerKey)
throw new Exception("Rust Segment dispose failed");
}

public void Identify(string userId, JsonObject? traits = null)
public void Identify(string? userId, JsonObject? traits = null)
{
lock (afterClean)
{
cachedUserId = userId;

var list = ListPool<MarshaledString>.Get()!;

var mUserId = new MarshaledString(userId);
var mUserId = new MarshaledString(cachedUserId);
var mAnonId = new MarshaledString(anonId);
var mTraits = new MarshaledString(traits?.ToString() ?? EMPTY_JSON);
var mContext = new MarshaledString(contextSource.ContextJson());

ulong operationId = NativeMethods.SegmentServerIdentify(mUserId.Ptr, mTraits.Ptr, mContext.Ptr);
ulong operationId = NativeMethods.SegmentServerIdentify(mUserId.Ptr, mAnonId.Ptr, mTraits.Ptr, mContext.Ptr);
AlertIfInvalid(operationId);

list.Add(mUserId);
Expand All @@ -82,19 +90,19 @@ public void Track(string eventName, JsonObject? properties = null)
{
lock (afterClean)
{

#if UNITY_EDITOR || DEBUG
ReportIfIdentityWasNotCalled();
#endif

var list = ListPool<MarshaledString>.Get()!;

var mUserId = new MarshaledString(cachedUserId);
var mAnonId = new MarshaledString(anonId);
var mEventName = new MarshaledString(eventName);
var mProperties = new MarshaledString(properties?.ToString() ?? EMPTY_JSON);
var mContext = new MarshaledString(contextSource.ContextJson());

ulong operationId = NativeMethods.SegmentServerTrack(mUserId.Ptr, mEventName.Ptr, mProperties.Ptr, mContext.Ptr);
ulong operationId = NativeMethods.SegmentServerTrack(mUserId.Ptr, mAnonId.Ptr, mEventName.Ptr, mProperties.Ptr, mContext.Ptr);
AlertIfInvalid(operationId);

list.Add(mUserId);
Expand Down Expand Up @@ -150,7 +158,7 @@ private void CleanMemory(ulong operationId)
ListPool<MarshaledString>.Release(list.Item2);
}

private void AlertIfInvalid(ulong operationId)
private static void AlertIfInvalid(ulong operationId)
{
if (operationId == 0)
ReportHub.LogError(
Expand All @@ -161,7 +169,7 @@ private void AlertIfInvalid(ulong operationId)

private void ReportIfIdentityWasNotCalled()
{
if (string.IsNullOrWhiteSpace(cachedUserId))
if (string.IsNullOrWhiteSpace(cachedUserId!) && string.IsNullOrWhiteSpace(anonId!))
ReportHub.LogError(
ReportCategory.ANALYTICS,
$"Segment to track an event, you must call Identify first"
Expand Down
40 changes: 28 additions & 12 deletions Explorer/Assets/Plugins/RustSegment/rust-segment/src/cabi.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use core::str;
use std::ffi::{c_char, CStr};
use std::ffi::c_char;

use crate::{server::SegmentServer, FfiCallbackFn, OperationHandleId, SEGMENT_SERVER};
use crate::{
operations::{as_str, user_from},
server::SegmentServer,
FfiCallbackFn, OperationHandleId, INVALID_OPERATION_HANDLE_ID, SEGMENT_SERVER,
};

/// # Safety
///
Expand All @@ -21,17 +24,26 @@ pub unsafe extern "C" fn segment_server_initialize(
#[no_mangle]
pub unsafe extern "C" fn segment_server_identify(
used_id: *const c_char,
anon_id: *const c_char,
traits_json: *const c_char,
context_json: *const c_char,
) -> OperationHandleId {
let user = user_from(used_id, anon_id);

if user.is_none() {
return INVALID_OPERATION_HANDLE_ID;
}

let user = user.unwrap();

SEGMENT_SERVER.try_execute(&|segment, id| {
let user = user.clone();
let segment = segment.clone();
let used_id = as_str(used_id);
let traits_json = as_str(traits_json);
let context_json = as_str(context_json);

let operation =
SegmentServer::enqueue_identify(segment, id, used_id, traits_json, context_json);
SegmentServer::enqueue_identify(segment, id, user, traits_json, context_json);
SEGMENT_SERVER.async_runtime.spawn(operation);
})
}
Expand All @@ -42,21 +54,30 @@ pub unsafe extern "C" fn segment_server_identify(
#[no_mangle]
pub unsafe extern "C" fn segment_server_track(
used_id: *const c_char,
anon_id: *const c_char,
event_name: *const c_char,
properties_json: *const c_char,
context_json: *const c_char,
) -> OperationHandleId {
let user = user_from(used_id, anon_id);

if user.is_none() {
return INVALID_OPERATION_HANDLE_ID;
}

let user = user.unwrap();

SEGMENT_SERVER.try_execute(&|segment, id| {
let user = user.clone();
let segment = segment.clone();
let used_id = as_str(used_id);
let event_name = as_str(event_name);
let properties_json = as_str(properties_json);
let context_json = as_str(context_json);

let operation = SegmentServer::enqueue_track(
segment,
id,
used_id,
user,
event_name,
properties_json,
context_json,
Expand All @@ -78,8 +99,3 @@ pub extern "C" fn segment_server_flush() -> OperationHandleId {
pub extern "C" fn segment_server_dispose() -> bool {
SEGMENT_SERVER.dispose()
}

fn as_str<'a>(chars: *const c_char) -> &'a str {
let c_str = unsafe { CStr::from_ptr(chars) };
c_str.to_str().unwrap()
}
2 changes: 2 additions & 0 deletions Explorer/Assets/Plugins/RustSegment/rust-segment/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use lazy_static::lazy_static;

pub type OperationHandleId = u64;

pub const INVALID_OPERATION_HANDLE_ID: OperationHandleId = 0;

#[repr(u8)]
#[derive(Debug)]
pub enum Response {
Expand Down
47 changes: 39 additions & 8 deletions Explorer/Assets/Plugins/RustSegment/rust-segment/src/operations.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use segment::message::{Identify, Track, User};
use serde_json::Value;
use std::ffi::{c_char, CStr};
use time::OffsetDateTime;

pub fn new_track(
used_id: &str,
user: User,
event_name: &str,
properties_json: &str,
context_json: &str,
Expand All @@ -12,9 +13,7 @@ pub fn new_track(
let context_json: Value = as_option(serde_json::from_str(context_json))?;

Some(Track {
user: User::UserId {
user_id: used_id.to_string(),
},
user,
event: event_name.to_string(),
properties: properties_json,
context: Some(context_json),
Expand All @@ -23,21 +22,53 @@ pub fn new_track(
})
}

pub fn new_identify(used_id: &str, traits_json: &str, context_json: &str) -> Option<Identify> {
pub fn new_identify(user: User, traits_json: &str, context_json: &str) -> Option<Identify> {
let traits_json: Value = as_option(serde_json::from_str(traits_json))?;
let context_json: Value = as_option(serde_json::from_str(context_json))?;

Some(Identify {
user: User::UserId {
user_id: used_id.to_string(),
},
user,
traits: traits_json,
context: Some(context_json),
timestamp: Some(OffsetDateTime::now_utc()),
..Default::default()
})
}

pub unsafe fn user_from(used_id: *const c_char, anon_id: *const c_char) -> Option<User> {
if used_id.is_null() && anon_id.is_null() {
return None;
}

if !used_id.is_null() && !anon_id.is_null() {
let user = as_str(used_id);
let anon = as_str(anon_id);
let output = User::Both {
user_id: user.to_string(),
anonymous_id: anon.to_string(),
};
return Some(output);
}

if !used_id.is_null() {
let user = as_str(used_id);
let output = User::UserId {
user_id: user.to_string(),
};
return Some(output);
}

let anon = as_str(anon_id);
Some(User::AnonymousId {
anonymous_id: anon.to_string(),
})
}

pub unsafe fn as_str<'a>(chars: *const c_char) -> &'a str {
let c_str = unsafe { CStr::from_ptr(chars) };
c_str.to_str().unwrap()
}

fn as_option<T, E>(result: Result<T, E>) -> Option<T> {
match result {
Ok(value) => Some(value),
Expand Down
Loading

0 comments on commit 59dc42c

Please sign in to comment.