Skip to content

Commit

Permalink
Implement API for setting user info and assigning user ID (#5)
Browse files Browse the repository at this point in the history
* API to set user info

* Update demo to show how to set user information

* Generate unique ID for users, print useful user info

* Store user info in a runtime config

Runtime info is stored in the user data dir in "sentry.config" file (like user ID, email, etc.). A call to Sentry.set_user() also saves that data to the conf file, which is loaded on a subsequent launch.
Demo project is updated to show that data.

* Clean up

* Generate device and user UUIDs via sentry-native

* Pass arguments as const ref

* Better whitespace when printing SentryUser object

* Automatically assign user ID

* Inherit user ID from runtime config if not set explicitly or generate a new one

* Clean include

* Fix runtime config issues

* Add `SentryUser.generate_user_id()` for convenience

* Improve demo project UI layout

* Store user data in sentry.dat

* Corrections
  • Loading branch information
limbonaut authored Oct 29, 2024
1 parent 83747c3 commit 9cee478
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 6 deletions.
27 changes: 27 additions & 0 deletions project/main.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ extends CanvasLayer
@onready var tag_value: LineEdit = %TagValue
@onready var context_name: LineEdit = %ContextName
@onready var context_expression: CodeEdit = %ContextExpression
@onready var user_id: LineEdit = %UserID
@onready var username: LineEdit = %Username
@onready var email: LineEdit = %Email
@onready var infer_ip: CheckBox = %InferIP

var _event_level: Sentry.Level


func _ready() -> void:
level_choice.get_popup().id_pressed.connect(_on_level_choice_id_pressed)
_init_level_choice_popup()
_update_user_info()


func _init_level_choice_popup() -> void:
Expand All @@ -28,6 +33,15 @@ func _init_level_choice_popup() -> void:
_on_level_choice_id_pressed(Sentry.LEVEL_INFO + 1)


func _update_user_info() -> void:
# The user info is persisted in the user data directory (referenced by "user://"),
# so it will be loaded again on subsequent launches.
var user: SentryUser = Sentry.get_user()
username.text = user.username
email.text = user.email
user_id.text = user.id


func _on_level_choice_id_pressed(id: int) -> void:
_event_level = (id - 1) as Sentry.Level
match _event_level:
Expand Down Expand Up @@ -86,3 +100,16 @@ func _on_set_context_pressed() -> void:
print("Failed set context: Dictionary is expected, but found: ", type_string(typeof(result)))
else:
print("Failed to parse expression: ", expr.get_error_text())


func _on_set_user_button_pressed() -> void:
print("Setting user info...")
var sentry_user := SentryUser.new()
sentry_user.id = user_id.text
sentry_user.username = username.text
sentry_user.email = email.text
if infer_ip.button_pressed:
sentry_user.infer_ip_address()
Sentry.set_user(sentry_user)
print(" ", sentry_user)
_update_user_info()
45 changes: 45 additions & 0 deletions project/main.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,50 @@ layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 16

[node name="Header - User Info" type="Label" parent="VBoxContainer/Columns/Column2"]
custom_minimum_size = Vector2(0, 40.505)
layout_mode = 2
text = "USER INFO"
horizontal_alignment = 1
vertical_alignment = 2

[node name="User info" type="HBoxContainer" parent="VBoxContainer/Columns/Column2"]
layout_mode = 2

[node name="Label" type="Label" parent="VBoxContainer/Columns/Column2/User info"]
layout_mode = 2
text = "User ID: "

[node name="UserID" type="LineEdit" parent="VBoxContainer/Columns/Column2/User info"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "ID"

[node name="InferIP" type="CheckBox" parent="VBoxContainer/Columns/Column2/User info"]
unique_name_in_owner = true
layout_mode = 2
text = "Infer IP"

[node name="User info (continued)" type="HBoxContainer" parent="VBoxContainer/Columns/Column2"]
layout_mode = 2

[node name="Username" type="LineEdit" parent="VBoxContainer/Columns/Column2/User info (continued)"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Username"

[node name="Email" type="LineEdit" parent="VBoxContainer/Columns/Column2/User info (continued)"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Email"

[node name="SetUserButton" type="Button" parent="VBoxContainer/Columns/Column2"]
layout_mode = 2
text = "Set User"

[node name="Header - Capture" type="Label" parent="VBoxContainer/Columns/Column2"]
custom_minimum_size = Vector2(0, 40.505)
layout_mode = 2
Expand Down Expand Up @@ -217,5 +261,6 @@ grow_vertical = 2
[connection signal="pressed" from="VBoxContainer/Columns/Column1/Breadcrumb/AddBreadcrumbButton" to="." method="_on_add_breadcrumb_button_pressed"]
[connection signal="pressed" from="VBoxContainer/Columns/Column1/Tags/AddTagButton" to="." method="_on_add_tag_button_pressed"]
[connection signal="pressed" from="VBoxContainer/Columns/Column1/Context/SetContext" to="." method="_on_set_context_pressed"]
[connection signal="pressed" from="VBoxContainer/Columns/Column2/SetUserButton" to="." method="_on_set_user_button_pressed"]
[connection signal="pressed" from="VBoxContainer/Columns/Column2/MessageEvent/CaptureButton" to="." method="_on_capture_button_pressed"]
[connection signal="pressed" from="VBoxContainer/Columns/Column2/Crash2/CrashButton" to="." method="_on_crash_button_pressed"]
2 changes: 2 additions & 0 deletions src/register_types.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "sentry_options.h"
#include "sentry_singleton.h"
#include "sentry_user.h"

#include <sentry.h>
#include <godot_cpp/classes/engine.hpp>
Expand All @@ -14,6 +15,7 @@ void initialize_module(ModuleInitializationLevel p_level) {
} else if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) {
// Note: Godot singletons are only available at higher initialization levels.
SentryOptions *options = new SentryOptions();
GDREGISTER_CLASS(SentryUser);
GDREGISTER_CLASS(Sentry);
Sentry *sentry_singleton = memnew(Sentry);
Engine::get_singleton()->register_singleton("Sentry", Sentry::get_singleton());
Expand Down
51 changes: 51 additions & 0 deletions src/runtime_config.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#include "runtime_config.h"
#include "sentry_user.h"

#include <godot_cpp/core/memory.hpp>

namespace {

inline String _ensure_string(const Variant &p_value, const String &p_fallback) {
return p_value.get_type() == Variant::STRING ? (String)p_value : p_fallback;
}

inline CharString _ensure_cstring(const Variant &p_value, const CharString &p_fallback) {
return p_value.get_type() == Variant::STRING ? ((String)p_value).utf8() : p_fallback;
}

} // unnamed namespace

void RuntimeConfig::set_user(const Ref<SentryUser> &p_user) {
ERR_FAIL_COND(p_user.is_null());
user = p_user;

conf->set_value("user", "id", p_user->get_id());
conf->set_value("user", "email", p_user->get_email());
conf->set_value("user", "username", p_user->get_username());
conf->save(conf_path);
}

void RuntimeConfig::set_device_id(const CharString &p_device_id) {
ERR_FAIL_COND(p_device_id.length() == 0);
device_id = p_device_id;
conf->set_value("device", "id", (String)device_id);
conf->save(conf_path);
}

void RuntimeConfig::load_file(const String &p_conf_path) {
ERR_FAIL_COND(p_conf_path.is_empty());

conf_path = p_conf_path;
conf->load(conf_path);

user = Ref(memnew(SentryUser));
user->set_id(_ensure_string(conf->get_value("user", "id", ""), ""));
user->set_email(_ensure_string(conf->get_value("user", "email", ""), ""));
user->set_username(_ensure_string(conf->get_value("user", "username", ""), ""));

device_id = _ensure_cstring(conf->get_value("device", "id", ""), "");
}

RuntimeConfig::RuntimeConfig() {
conf.instantiate();
}
32 changes: 32 additions & 0 deletions src/runtime_config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#ifndef RUNTIME_CONFIG_H
#define RUNTIME_CONFIG_H

#include "sentry_user.h"

#include <godot_cpp/classes/config_file.hpp>
#include <godot_cpp/variant/char_string.hpp>

using namespace godot;

class RuntimeConfig {
private:
String conf_path;
Ref<ConfigFile> conf;

// Cached values.
Ref<SentryUser> user;
CharString device_id;

public:
Ref<SentryUser> get_user() const { return user; }
void set_user(const Ref<SentryUser> &p_user);

CharString get_device_id() const { return device_id; }
void set_device_id(const CharString &p_device_id);

void load_file(const String &p_conf_path);

RuntimeConfig();
};

#endif // RUNTIME_CONFIG_H
63 changes: 57 additions & 6 deletions src/sentry_singleton.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ void Sentry::add_device_context() {
sentry_value_set_by_key(device_context, "cpu_description",
sentry_value_new_string(OS::get_singleton()->get_processor_name().utf8()));

#ifndef WEB_ENABLED
// Generates an error on wasm builds.
String unique_id = OS::get_singleton()->get_unique_id();
if (!unique_id.is_empty()) {
sentry_value_set_by_key(device_context, "device_unique_identifier", sentry_value_new_string(unique_id.utf8()));
// Read/initialize device unique identifier.
CharString device_id = runtime_config.get_device_id();
if (device_id.length() == 0) {
device_id = SentryUtil::generate_uuid();
runtime_config.set_device_id(device_id);
}
#endif
sentry_value_set_by_key(device_context, "device_unique_identifier", sentry_value_new_string(device_id));

sentry_set_context("device", device_context);
}
Expand Down Expand Up @@ -416,6 +416,48 @@ void Sentry::remove_tag(const godot::String &p_key) {
sentry_remove_tag(p_key.utf8());
}

void Sentry::set_user(const godot::Ref<SentryUser> &p_user) {
ERR_FAIL_NULL_MSG(p_user, "Sentry: Setting user failed - user object is null. Please, use Sentry.remove_user() to clear user info.");

// Initialize user ID if not supplied.
if (p_user->get_id().is_empty()) {
// Take user ID from the runtime config or generate a new one if it's empty.
String user_id = get_user()->get_id();
if (user_id.is_empty()) {
user_id = SentryUtil::generate_uuid();
}
p_user->set_id(user_id);
}

// Save user in a runtime conf-file.
// TODO: Make it optional?
runtime_config.set_user(p_user);

sentry_value_t user_data = sentry_value_new_object();

if (!p_user->get_id().is_empty()) {
sentry_value_set_by_key(user_data, "id",
sentry_value_new_string(p_user->get_id().utf8()));
}
if (!p_user->get_username().is_empty()) {
sentry_value_set_by_key(user_data, "username",
sentry_value_new_string(p_user->get_username().utf8()));
}
if (!p_user->get_email().is_empty()) {
sentry_value_set_by_key(user_data, "email",
sentry_value_new_string(p_user->get_email().utf8()));
}
if (!p_user->get_ip_address().is_empty()) {
sentry_value_set_by_key(user_data, "ip_address",
sentry_value_new_string(p_user->get_ip_address().utf8()));
}
sentry_set_user(user_data);
}

void Sentry::remove_user() {
sentry_remove_user();
}

void Sentry::set_context(const godot::String &p_key, const godot::Dictionary &p_value) {
ERR_FAIL_COND_MSG(p_key.is_empty(), "Sentry: Can't set context with an empty key.");
sentry_set_context(p_key.utf8(), SentryUtil::variant_to_sentry_value(p_value));
Expand Down Expand Up @@ -444,6 +486,9 @@ void Sentry::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_context", "key", "value"), &Sentry::set_context);
ClassDB::bind_method(D_METHOD("set_tag", "key", "value"), &Sentry::set_tag);
ClassDB::bind_method(D_METHOD("remove_tag", "key"), &Sentry::remove_tag);
ClassDB::bind_method(D_METHOD("set_user", "user"), &Sentry::set_user);
ClassDB::bind_method(D_METHOD("get_user"), &Sentry::get_user);
ClassDB::bind_method(D_METHOD("remove_user"), &Sentry::remove_user);
}

Sentry::Sentry() {
Expand All @@ -457,6 +502,9 @@ Sentry::Sentry() {
return;
}

// Load the runtime configuration from the user's data directory.
runtime_config.load_file(OS::get_singleton()->get_user_data_dir() + "/sentry.dat");

sentry_options_t *options = sentry_options_new();
sentry_options_set_dsn(options, SentryOptions::get_singleton()->get_dsn());

Expand Down Expand Up @@ -514,6 +562,9 @@ Sentry::Sentry() {
sentry_options_set_on_crash(options, on_crash_lambda, NULL);

sentry_init(options);

// Initialize user.
set_user(runtime_config.get_user());
}

Sentry::~Sentry() {
Expand Down
8 changes: 8 additions & 0 deletions src/sentry_singleton.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#ifndef SENTRY_SINGLETON_H
#define SENTRY_SINGLETON_H

#include "runtime_config.h"
#include "sentry_user.h"

#include <sentry.h>
#include <godot_cpp/core/binder_common.hpp>
#include <godot_cpp/core/object.hpp>
Expand All @@ -14,6 +17,7 @@ class Sentry : public godot::Object {
private:
static Sentry *singleton;

RuntimeConfig runtime_config;
sentry_uuid_t last_uuid;

sentry_value_t _create_performance_context();
Expand Down Expand Up @@ -52,6 +56,10 @@ class Sentry : public godot::Object {
void set_tag(const godot::String &p_key, const godot::String &p_value);
void remove_tag(const godot::String &p_key);

void set_user(const godot::Ref<SentryUser> &p_user);
Ref<SentryUser> get_user() const { return runtime_config.get_user(); }
void remove_user();

void capture_message(const godot::String &p_message, Level p_level, const godot::String &p_logger = "");
godot::String get_last_event_id() const;

Expand Down
54 changes: 54 additions & 0 deletions src/sentry_user.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#include "sentry_user.h"

#include "sentry_util.h"

#include <godot_cpp/core/error_macros.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/variant.hpp>

bool SentryUser::is_user_valid() const {
return !id.is_empty() || !username.is_empty() || !email.is_empty() || !ip_address.is_empty();
}

String SentryUser::_to_string() const {
PackedStringArray parts;
if (!id.is_empty()) {
parts.append("id: " + id);
}
if (!username.is_empty()) {
parts.append("name: " + username);
}
if (!email.is_empty()) {
parts.append("email: " + email);
}
if (!ip_address.is_empty()) {
parts.append("ip: " + ip_address);
}
return "SentryUser:{ " + String("; ").join(parts) + " }";
}

void SentryUser::generate_new_id() {
id = SentryUtil::generate_uuid();
}

void SentryUser::_bind_methods() {
// Setters / getters
ClassDB::bind_method(D_METHOD("set_id", "id"), &SentryUser::set_id);
ClassDB::bind_method(D_METHOD("get_id"), &SentryUser::get_id);
ClassDB::bind_method(D_METHOD("set_username", "username"), &SentryUser::set_username);
ClassDB::bind_method(D_METHOD("get_username"), &SentryUser::get_username);
ClassDB::bind_method(D_METHOD("set_email", "email"), &SentryUser::set_email);
ClassDB::bind_method(D_METHOD("get_email"), &SentryUser::get_email);
ClassDB::bind_method(D_METHOD("set_ip_address", "ip_address"), &SentryUser::set_ip_address);
ClassDB::bind_method(D_METHOD("get_ip_address"), &SentryUser::get_ip_address);

ADD_PROPERTY(PropertyInfo(Variant::STRING, "id"), "set_id", "get_id");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "username"), "set_username", "get_username");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "email"), "set_email", "get_email");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "ip_address"), "set_ip_address", "get_ip_address");

// Other methods
ClassDB::bind_method(D_METHOD("infer_ip_address"), &SentryUser::infer_ip_address);
ClassDB::bind_method(D_METHOD("is_user_valid"), &SentryUser::is_user_valid);
ClassDB::bind_method(D_METHOD("generate_new_id"), &SentryUser::generate_new_id);
}
Loading

0 comments on commit 9cee478

Please sign in to comment.