← Volver al listado de tecnologías
Analytics, Eventos y Tracking de Usuarios
Analytics, Eventos y Tracking de Usuarios
El analytics es fundamental para entender el comportamiento de los usuarios y optimizar tu juego. Esta guía te enseñará a implementar un sistema completo de tracking y análisis.
Arquitectura de Analytics
1. Analytics Manager Core
Sistema Central de Analytics
-- analytics_manager.script
local M = {}
function init(self)
self.providers = {}
self.event_queue = {}
self.session_data = {}
self.user_properties = {}
-- Configuración
self.config = {
batch_size = 20,
flush_interval = 30.0, -- Enviar eventos cada 30 segundos
max_queue_size = 100,
retry_attempts = 3,
debug_mode = sys.get_config("project.debug", "0") == "1"
}
-- Timers
self.flush_timer = 0
self.session_timer = 0
-- Estado de sesión
self.session_id = self:generate_session_id()
self.session_start_time = socket.gettime()
print("Analytics Manager initialized")
end
function M.initialize_providers(self, providers_config)
for _, provider_config in ipairs(providers_config) do
self:add_provider(provider_config)
end
-- Inicializar sesión
self:start_session()
print("Analytics providers initialized: " .. #self.providers)
end
function M.add_provider(self, config)
local provider = {
name = config.name,
enabled = config.enabled,
initialized = false,
config = config
}
if config.name == "firebase" then
self:initialize_firebase(provider)
elseif config.name == "facebook" then
self:initialize_facebook(provider)
elseif config.name == "appsflyer" then
self:initialize_appsflyer(provider)
elseif config.name == "adjust" then
self:initialize_adjust(provider)
elseif config.name == "custom" then
self:initialize_custom_analytics(provider)
end
table.insert(self.providers, provider)
end
function M.track_event(self, event_name, properties, immediate)
properties = properties or {}
immediate = immediate or false
-- Añadir propiedades de sesión
properties.session_id = self.session_id
properties.session_time = socket.gettime() - self.session_start_time
properties.timestamp = os.time()
local event = {
name = event_name,
properties = properties,
timestamp = socket.gettime()
}
if immediate then
self:send_event_to_providers(event)
else
self:queue_event(event)
end
if self.config.debug_mode then
print("Event tracked: " .. event_name)
pprint(properties)
end
end
function M.queue_event(self, event)
table.insert(self.event_queue, event)
-- Flush si la cola está llena
if #self.event_queue >= self.config.batch_size then
self:flush_events()
end
-- Limpiar cola si está muy llena
if #self.event_queue > self.config.max_queue_size then
table.remove(self.event_queue, 1) -- Remover evento más antiguo
end
end
function M.flush_events(self)
if #self.event_queue == 0 then return end
local events_to_send = {}
for _, event in ipairs(self.event_queue) do
table.insert(events_to_send, event)
end
self:send_events_batch(events_to_send)
self.event_queue = {} -- Limpiar cola
if self.config.debug_mode then
print("Flushed " .. #events_to_send .. " events")
end
end
function M.send_events_batch(self, events)
for _, provider in ipairs(self.providers) do
if provider.enabled and provider.initialized then
self:send_batch_to_provider(provider, events)
end
end
end
function M.send_event_to_providers(self, event)
for _, provider in ipairs(self.providers) do
if provider.enabled and provider.initialized then
self:send_single_event_to_provider(provider, event)
end
end
end
function update(self, dt)
self.flush_timer = self.flush_timer + dt
self.session_timer = self.session_timer + dt
-- Flush automático
if self.flush_timer >= self.config.flush_interval then
self:flush_events()
self.flush_timer = 0
end
-- Actualizar tiempo de sesión cada minuto
if self.session_timer >= 60.0 then
self:update_session_time()
self.session_timer = 0
end
end
function M.start_session(self)
self.session_start_time = socket.gettime()
self:track_event("session_start", {
device_info = self:get_device_info(),
app_version = sys.get_config("project.version"),
session_id = self.session_id
}, true)
end
function M.end_session(self)
local session_duration = socket.gettime() - self.session_start_time
self:track_event("session_end", {
session_duration = session_duration,
session_id = self.session_id
}, true)
-- Flush todos los eventos pendientes
self:flush_events()
end
return M
2. Event Schema System
Sistema de Esquemas de Eventos
-- event_schemas.lua
local M = {}
-- Definir esquemas de eventos para validación y consistencia
M.SCHEMAS = {
-- Eventos de gameplay
level_start = {
required = {"level_number", "level_name"},
optional = {"difficulty", "character", "power_ups"},
types = {
level_number = "number",
level_name = "string",
difficulty = "string",
character = "string",
power_ups = "table"
}
},
level_complete = {
required = {"level_number", "level_name", "completion_time", "score"},
optional = {"deaths", "power_ups_used", "stars_earned"},
types = {
level_number = "number",
level_name = "string",
completion_time = "number",
score = "number",
deaths = "number",
power_ups_used = "table",
stars_earned = "number"
}
},
level_failed = {
required = {"level_number", "level_name", "failure_reason", "attempt_time"},
optional = {"progress_percent", "death_location", "power_ups_used"},
types = {
level_number = "number",
level_name = "string",
failure_reason = "string",
attempt_time = "number",
progress_percent = "number",
death_location = "string",
power_ups_used = "table"
}
},
-- Eventos de monetización
purchase_attempt = {
required = {"product_id", "price", "currency"},
optional = {"placement", "offer_id"},
types = {
product_id = "string",
price = "number",
currency = "string",
placement = "string",
offer_id = "string"
}
},
purchase_complete = {
required = {"product_id", "price", "currency", "transaction_id"},
optional = {"placement", "offer_id", "first_purchase"},
types = {
product_id = "string",
price = "number",
currency = "string",
transaction_id = "string",
placement = "string",
offer_id = "string",
first_purchase = "boolean"
}
},
-- Eventos de engagement
tutorial_step = {
required = {"step_number", "step_name", "action"},
optional = {"time_spent", "help_used"},
types = {
step_number = "number",
step_name = "string",
action = "string",
time_spent = "number",
help_used = "boolean"
}
},
-- Eventos de retención
daily_login = {
required = {"day_number", "consecutive_days"},
optional = {"login_bonus_claimed", "total_logins"},
types = {
day_number = "number",
consecutive_days = "number",
login_bonus_claimed = "boolean",
total_logins = "number"
}
}
}
function M.validate_event(event_name, properties)
local schema = M.SCHEMAS[event_name]
if not schema then
print("Warning: No schema defined for event: " .. event_name)
return true, properties -- Permitir eventos sin schema
end
local validated_properties = {}
local errors = {}
-- Verificar propiedades requeridas
for _, required_prop in ipairs(schema.required) do
if properties[required_prop] == nil then
table.insert(errors, "Missing required property: " .. required_prop)
else
validated_properties[required_prop] = properties[required_prop]
end
end
-- Verificar propiedades opcionales
for _, optional_prop in ipairs(schema.optional) do
if properties[optional_prop] ~= nil then
validated_properties[optional_prop] = properties[optional_prop]
end
end
-- Verificar tipos de datos
for prop_name, expected_type in pairs(schema.types) do
local value = properties[prop_name]
if value ~= nil then
local actual_type = type(value)
if actual_type ~= expected_type then
table.insert(errors, string.format(
"Property %s expected %s, got %s",
prop_name, expected_type, actual_type
))
end
end
end
-- Añadir propiedades no definidas en schema (con warning)
for prop_name, value in pairs(properties) do
if validated_properties[prop_name] == nil then
print("Warning: Undefined property in schema: " .. prop_name)
validated_properties[prop_name] = value
end
end
local is_valid = #errors == 0
if not is_valid then
print("Event validation failed for: " .. event_name)
for _, error in ipairs(errors) do
print(" " .. error)
end
end
return is_valid, validated_properties
end
function M.get_schema_info(event_name)
return M.SCHEMAS[event_name]
end
return M
Providers de Analytics
1. Firebase Analytics
Integración Firebase
-- firebase_analytics.lua
local M = {}
function M.initialize(provider)
if firebase and firebase.analytics then
firebase.analytics.set_analytics_collection_enabled(true)
-- Configurar propiedades del usuario
firebase.analytics.set_user_property("app_version", sys.get_config("project.version"))
local device_info = sys.get_sys_info()
firebase.analytics.set_user_property("device_model", device_info.device_model)
firebase.analytics.set_user_property("os_version", device_info.system_version)
provider.initialized = true
print("Firebase Analytics initialized")
else
print("Firebase Analytics not available")
end
end
function M.send_event(provider, event)
if not firebase or not firebase.analytics then return end
local firebase_properties = M.convert_properties_for_firebase(event.properties)
firebase.analytics.log_event(event.name, firebase_properties)
end
function M.send_batch(provider, events)
for _, event in ipairs(events) do
M.send_event(provider, event)
end
end
function M.convert_properties_for_firebase(properties)
local converted = {}
for key, value in pairs(properties) do
-- Firebase tiene limitaciones en nombres de propiedades y valores
local firebase_key = M.sanitize_firebase_key(key)
if type(value) == "string" then
converted[firebase_key] = string.sub(value, 1, 100) -- Max 100 chars
elseif type(value) == "number" then
converted[firebase_key] = value
elseif type(value) == "boolean" then
converted[firebase_key] = value
elseif type(value) == "table" then
converted[firebase_key] = json.encode(value)
else
converted[firebase_key] = tostring(value)
end
end
return converted
end
function M.sanitize_firebase_key(key)
-- Firebase requiere que las keys empiecen con letra y solo contengan letras, números y _
key = string.gsub(key, "[^%w_]", "_")
key = string.gsub(key, "^%d", "n%1") -- Añadir 'n' si empieza con número
return string.sub(key, 1, 40) -- Max 40 chars
end
function M.set_user_property(provider, property, value)
if firebase and firebase.analytics then
firebase.analytics.set_user_property(property, tostring(value))
end
end
return M
2. Facebook Analytics
Integración Facebook
-- facebook_analytics.lua
local M = {}
function M.initialize(provider)
if facebook then
facebook.enable_event_usage()
facebook.enable_advertiser_tracking(true)
provider.initialized = true
print("Facebook Analytics initialized")
else
print("Facebook Analytics not available")
end
end
function M.send_event(provider, event)
if not facebook then return end
local facebook_properties = M.convert_properties_for_facebook(event.properties)
-- Mapear eventos a eventos estándar de Facebook cuando sea posible
local facebook_event = M.map_to_facebook_event(event.name)
if facebook_event then
facebook.log_event(facebook_event, facebook_properties.value, facebook_properties.parameters)
else
facebook.log_event_value_to_sum(event.name, 0, facebook_properties)
end
end
function M.map_to_facebook_event(event_name)
local mapping = {
purchase_complete = facebook.EVENT_PURCHASED,
level_complete = facebook.EVENT_ACHIEVED_LEVEL,
tutorial_step = facebook.EVENT_COMPLETED_TUTORIAL,
registration = facebook.EVENT_COMPLETED_REGISTRATION,
level_start = facebook.EVENT_INITIATED_CHECKOUT
}
return mapping[event_name]
end
function M.convert_properties_for_facebook(properties)
local parameters = {}
local value = 0
for key, prop_value in pairs(properties) do
if key == "price" or key == "value" then
value = tonumber(prop_value) or 0
elseif key == "currency" then
parameters[facebook.PARAM_CURRENCY] = prop_value
elseif key == "level_number" then
parameters[facebook.PARAM_LEVEL] = prop_value
elseif key == "content_type" then
parameters[facebook.PARAM_CONTENT_TYPE] = prop_value
else
parameters[key] = tostring(prop_value)
end
end
return {
value = value,
parameters = parameters
}
end
return M
3. Custom Analytics
Sistema Custom de Analytics
-- custom_analytics.lua
local M = {}
function M.initialize(provider)
self.endpoint = provider.config.endpoint
self.api_key = provider.config.api_key
self.batch_endpoint = provider.config.batch_endpoint
self.retry_queue = {}
provider.initialized = true
print("Custom Analytics initialized: " .. self.endpoint)
end
function M.send_event(provider, event)
local data = {
event_name = event.name,
properties = event.properties,
timestamp = event.timestamp,
session_id = event.properties.session_id
}
self:send_to_endpoint(provider, "/event", data)
end
function M.send_batch(provider, events)
local batch_data = {
events = {},
batch_id = self:generate_batch_id(),
timestamp = socket.gettime()
}
for _, event in ipairs(events) do
table.insert(batch_data.events, {
event_name = event.name,
properties = event.properties,
timestamp = event.timestamp
})
end
self:send_to_endpoint(provider, "/batch", batch_data)
end
function send_to_endpoint(self, provider, path, data)
local url = self.endpoint .. path
local headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. self.api_key,
["X-Client-Version"] = sys.get_config("project.version"),
["X-Platform"] = sys.get_sys_info().system_name
}
local json_data = json.encode(data)
http.request(url, "POST", function(self, id, response)
self:handle_response(provider, data, response)
end, headers, json_data)
end
function handle_response(self, provider, data, response)
if response.status == 200 then
-- Éxito
if self.config.debug_mode then
print("Analytics sent successfully")
end
else
-- Error - añadir a cola de retry
print("Analytics send failed: " .. response.status)
table.insert(self.retry_queue, {
data = data,
attempts = 1,
next_retry = socket.gettime() + 30 -- Retry en 30 segundos
})
end
end
function M.process_retry_queue(provider)
local current_time = socket.gettime()
local to_retry = {}
for i, retry_item in ipairs(self.retry_queue) do
if current_time >= retry_item.next_retry then
if retry_item.attempts < 3 then
-- Intentar nuevamente
table.insert(to_retry, retry_item)
end
-- Remover de la cola
table.remove(self.retry_queue, i)
end
end
-- Reintentar envíos
for _, retry_item in ipairs(to_retry) do
retry_item.attempts = retry_item.attempts + 1
retry_item.next_retry = current_time + (retry_item.attempts * 60) -- Backoff exponencial
self:send_to_endpoint(provider, "/event", retry_item.data)
end
end
function generate_batch_id(self)
return "batch_" .. os.time() .. "_" .. math.random(1000, 9999)
end
return M
Game-Specific Analytics
1. Gameplay Analytics
Métricas de Gameplay
-- gameplay_analytics.lua
local M = {}
function init(self)
self.level_start_time = 0
self.current_level = 1
self.deaths_in_level = 0
self.power_ups_used = {}
self.analytics_manager = require "main.analytics_manager"
end
-- Tracking de niveles
function M.track_level_start(self, level_number, level_name, difficulty)
self.level_start_time = socket.gettime()
self.current_level = level_number
self.deaths_in_level = 0
self.power_ups_used = {}
self.analytics_manager:track_event("level_start", {
level_number = level_number,
level_name = level_name,
difficulty = difficulty,
player_level = self:get_player_level(),
total_coins = self:get_player_coins()
})
end
function M.track_level_complete(self, level_number, level_name, score, stars)
local completion_time = socket.gettime() - self.level_start_time
self.analytics_manager:track_event("level_complete", {
level_number = level_number,
level_name = level_name,
completion_time = completion_time,
score = score,
stars_earned = stars,
deaths = self.deaths_in_level,
power_ups_used = self.power_ups_used,
attempts = self:get_level_attempts(level_number)
})
-- Reset tracking data
self:reset_level_tracking()
end
function M.track_level_failed(self, level_number, level_name, failure_reason, progress_percent)
local attempt_time = socket.gettime() - self.level_start_time
self.analytics_manager:track_event("level_failed", {
level_number = level_number,
level_name = level_name,
failure_reason = failure_reason,
attempt_time = attempt_time,
progress_percent = progress_percent,
deaths = self.deaths_in_level,
power_ups_used = self.power_ups_used,
attempts = self:get_level_attempts(level_number)
})
end
function M.track_player_death(self, death_cause, location)
self.deaths_in_level = self.deaths_in_level + 1
self.analytics_manager:track_event("player_death", {
level_number = self.current_level,
death_cause = death_cause,
death_location = location,
death_number_in_level = self.deaths_in_level,
time_to_death = socket.gettime() - self.level_start_time
})
end
function M.track_power_up_used(self, power_up_type, trigger_reason)
if not self.power_ups_used[power_up_type] then
self.power_ups_used[power_up_type] = 0
end
self.power_ups_used[power_up_type] = self.power_ups_used[power_up_type] + 1
self.analytics_manager:track_event("power_up_used", {
power_up_type = power_up_type,
trigger_reason = trigger_reason,
level_number = self.current_level,
usage_count_in_level = self.power_ups_used[power_up_type]
})
end
function M.track_checkpoint_reached(self, checkpoint_id, time_to_reach)
self.analytics_manager:track_event("checkpoint_reached", {
level_number = self.current_level,
checkpoint_id = checkpoint_id,
time_to_reach = time_to_reach,
deaths_so_far = self.deaths_in_level
})
end
-- Métricas de progresión
function M.track_player_level_up(self, new_level, xp_gained, unlock_type)
self.analytics_manager:track_event("player_level_up", {
new_level = new_level,
previous_level = new_level - 1,
xp_gained = xp_gained,
unlock_type = unlock_type,
total_playtime = self:get_total_playtime()
})
end
function M.track_achievement_unlocked(self, achievement_id, achievement_type)
self.analytics_manager:track_event("achievement_unlocked", {
achievement_id = achievement_id,
achievement_type = achievement_type,
player_level = self:get_player_level(),
total_achievements = self:get_total_achievements()
})
end
-- Helpers
function reset_level_tracking(self)
self.level_start_time = 0
self.deaths_in_level = 0
self.power_ups_used = {}
end
function get_player_level(self)
-- Implementar según tu sistema de progresión
return 1
end
function get_player_coins(self)
-- Implementar según tu sistema de monedas
return 0
end
function get_level_attempts(self, level_number)
-- Implementar tracking de intentos por nivel
return 1
end
function get_total_playtime(self)
-- Implementar tracking de tiempo total de juego
return 0
end
function get_total_achievements(self)
-- Implementar conteo de logros
return 0
end
return M
2. Economy Analytics
Métricas de Economía
-- economy_analytics.lua
local M = {}
function init(self)
self.analytics_manager = require "main.analytics_manager"
self.transaction_history = {}
end
-- Tracking de monedas
function M.track_currency_earned(self, currency_type, amount, source, level_number)
self.analytics_manager:track_event("currency_earned", {
currency_type = currency_type,
amount = amount,
source = source, -- "level_complete", "daily_bonus", "achievement", etc.
level_number = level_number,
total_currency = self:get_total_currency(currency_type),
session_earnings = self:get_session_earnings(currency_type)
})
end
function M.track_currency_spent(self, currency_type, amount, sink, item_id)
self.analytics_manager:track_event("currency_spent", {
currency_type = currency_type,
amount = amount,
sink = sink, -- "power_up", "upgrade", "unlock", etc.
item_id = item_id,
total_currency_before = self:get_total_currency(currency_type),
total_currency_after = self:get_total_currency(currency_type) - amount
})
end
-- Tracking de items
function M.track_item_purchased(self, item_id, item_type, price, currency_type)
self.analytics_manager:track_event("item_purchased", {
item_id = item_id,
item_type = item_type,
price = price,
currency_type = currency_type,
player_level = self:get_player_level(),
total_items_owned = self:get_total_items_owned(item_type)
})
end
function M.track_item_used(self, item_id, item_type, usage_context)
self.analytics_manager:track_event("item_used", {
item_id = item_id,
item_type = item_type,
usage_context = usage_context,
remaining_quantity = self:get_item_quantity(item_id)
})
end
-- Tracking de upgrades
function M.track_upgrade_purchased(self, upgrade_type, upgrade_level, cost, currency_type)
self.analytics_manager:track_event("upgrade_purchased", {
upgrade_type = upgrade_type,
upgrade_level = upgrade_level,
previous_level = upgrade_level - 1,
cost = cost,
currency_type = currency_type,
total_upgrades = self:get_total_upgrades()
})
end
-- Economy balance tracking
function M.track_economy_snapshot(self)
local snapshot = {
coins = self:get_total_currency("coins"),
gems = self:get_total_currency("gems"),
power_ups = self:get_power_up_inventory(),
upgrades = self:get_upgrade_levels(),
player_level = self:get_player_level()
}
self.analytics_manager:track_event("economy_snapshot", snapshot)
end
-- Sink/Source Analysis
function M.analyze_currency_flow(self, time_period)
-- Esta función analiza el flujo de monedas en un período
local flow_data = {
time_period = time_period,
sources = {},
sinks = {},
net_flow = 0
}
-- Analizar transacciones en el período
for _, transaction in ipairs(self.transaction_history) do
if transaction.timestamp >= time_period.start and transaction.timestamp <= time_period.end then
if transaction.type == "earned" then
flow_data.sources[transaction.source] = (flow_data.sources[transaction.source] or 0) + transaction.amount
flow_data.net_flow = flow_data.net_flow + transaction.amount
elseif transaction.type == "spent" then
flow_data.sinks[transaction.sink] = (flow_data.sinks[transaction.sink] or 0) + transaction.amount
flow_data.net_flow = flow_data.net_flow - transaction.amount
end
end
end
self.analytics_manager:track_event("currency_flow_analysis", flow_data)
return flow_data
end
-- Helper functions (implementar según tu juego)
function get_total_currency(self, currency_type)
return 0 -- Implementar
end
function get_session_earnings(self, currency_type)
return 0 -- Implementar
end
function get_player_level(self)
return 1 -- Implementar
end
function get_total_items_owned(self, item_type)
return 0 -- Implementar
end
function get_item_quantity(self, item_id)
return 0 -- Implementar
end
function get_total_upgrades(self)
return 0 -- Implementar
end
function get_power_up_inventory(self)
return {} -- Implementar
end
function get_upgrade_levels(self)
return {} -- Implementar
end
return M
User Behavior Analytics
1. Session Analytics
Análisis de Sesiones
-- session_analytics.lua
local M = {}
function init(self)
self.analytics_manager = require "main.analytics_manager"
self.session_data = {
start_time = socket.gettime(),
events_count = 0,
screens_visited = {},
actions_performed = {},
purchases_made = {},
levels_played = {}
}
end
function M.track_session_start(self)
local device_info = sys.get_sys_info()
self.analytics_manager:track_event("session_start", {
device_model = device_info.device_model,
os_version = device_info.system_version,
app_version = sys.get_config("project.version"),
language = device_info.language,
is_first_session = self:is_first_session(),
days_since_install = self:get_days_since_install(),
total_sessions = self:get_total_sessions()
})
end
function M.track_session_end(self)
local session_duration = socket.gettime() - self.session_data.start_time
self.analytics_manager:track_event("session_end", {
session_duration = session_duration,
events_count = self.session_data.events_count,
screens_visited = self:get_unique_screens_count(),
actions_performed = self:get_total_actions(),
purchases_made = #self.session_data.purchases_made,
levels_played = #self.session_data.levels_played,
session_quality_score = self:calculate_session_quality()
})
end
function M.track_screen_view(self, screen_name, previous_screen)
self.session_data.screens_visited[screen_name] = (self.session_data.screens_visited[screen_name] or 0) + 1
self.analytics_manager:track_event("screen_view", {
screen_name = screen_name,
previous_screen = previous_screen,
time_on_previous_screen = self:get_time_on_screen(previous_screen),
visit_count = self.session_data.screens_visited[screen_name]
})
end
function M.track_user_action(self, action_type, action_details)
self.session_data.actions_performed[action_type] = (self.session_data.actions_performed[action_type] or 0) + 1
self.analytics_manager:track_event("user_action", {
action_type = action_type,
action_details = action_details,
action_count_in_session = self.session_data.actions_performed[action_type]
})
end
function M.calculate_session_quality(self)
local score = 0
-- Duración de sesión (peso: 0.3)
local duration = socket.gettime() - self.session_data.start_time
if duration > 300 then score = score + 30 -- 5+ minutos
elseif duration > 120 then score = score + 20 -- 2+ minutos
elseif duration > 60 then score = score + 10 -- 1+ minuto
end
-- Niveles jugados (peso: 0.3)
local levels_played = #self.session_data.levels_played
score = score + math.min(30, levels_played * 10)
-- Pantallas visitadas (peso: 0.2)
local screens_count = self:get_unique_screens_count()
score = score + math.min(20, screens_count * 4)
-- Compras realizadas (peso: 0.2)
local purchases = #self.session_data.purchases_made
score = score + math.min(20, purchases * 20)
return math.min(100, score)
end
-- Tracking de retention
function M.track_retention_cohort(self)
local cohort_data = {
install_date = self:get_install_date(),
days_since_install = self:get_days_since_install(),
session_number = self:get_total_sessions(),
retention_day = self:get_retention_day()
}
-- Solo trackear en días específicos (1, 3, 7, 14, 30)
local retention_day = cohort_data.retention_day
if retention_day == 1 or retention_day == 3 or retention_day == 7 or
retention_day == 14 or retention_day == 30 then
self.analytics_manager:track_event("retention_cohort", cohort_data)
end
end
-- Helper functions
function get_unique_screens_count(self)
local count = 0
for _ in pairs(self.session_data.screens_visited) do
count = count + 1
end
return count
end
function get_total_actions(self)
local total = 0
for _, count in pairs(self.session_data.actions_performed) do
total = total + count
end
return total
end
function is_first_session(self)
return self:get_total_sessions() == 1
end
function get_days_since_install(self)
-- Implementar basado en tu sistema de tracking
return 1
end
function get_total_sessions(self)
-- Implementar basado en tu sistema de tracking
return 1
end
function get_time_on_screen(self, screen_name)
-- Implementar tracking de tiempo por pantalla
return 0
end
function get_install_date(self)
-- Implementar
return os.date("%Y-%m-%d")
end
function get_retention_day(self)
-- Implementar cálculo de día de retención
return 1
end
return M
2. Player Segmentation
Segmentación de Jugadores
-- player_segmentation.lua
local M = {}
function init(self)
self.analytics_manager = require "main.analytics_manager"
self.segments = {
"new_user", -- 0-3 días
"casual_player", -- Pocas sesiones, poca progresión
"core_player", -- Sesiones regulares, buena progresión
"whale", -- Alto gasto en IAP
"at_risk", -- Actividad decreciente
"churned" -- No activo por 7+ días
}
end
function M.analyze_player_segment(self, player_data)
local segment = self:determine_segment(player_data)
self.analytics_manager:track_event("player_segment_analysis", {
current_segment = segment,
days_since_install = player_data.days_since_install,
total_sessions = player_data.total_sessions,
total_purchases = player_data.total_purchases,
total_spent = player_data.total_spent,
last_session_days_ago = player_data.last_session_days_ago,
avg_session_length = player_data.avg_session_length,
levels_completed = player_data.levels_completed
})
return segment
end
function determine_segment(self, data)
-- Nuevo usuario
if data.days_since_install <= 3 then
return "new_user"
end
-- Usuario churned
if data.last_session_days_ago >= 7 then
return "churned"
end
-- Usuario en riesgo
if data.last_session_days_ago >= 3 and data.avg_session_length < 300 then
return "at_risk"
end
-- Whale (gastador alto)
if data.total_spent > 50 or (data.total_purchases > 0 and data.total_spent / data.total_purchases > 10) then
return "whale"
end
-- Core player (jugador comprometido)
if data.total_sessions > 10 and data.avg_session_length > 600 and data.levels_completed > 20 then
return "core_player"
end
-- Por defecto: casual player
return "casual_player"
end
function M.track_segment_transition(self, player_id, old_segment, new_segment, reason)
self.analytics_manager:track_event("segment_transition", {
player_id = player_id,
old_segment = old_segment,
new_segment = new_segment,
transition_reason = reason,
days_in_old_segment = self:get_days_in_segment(player_id, old_segment)
})
end
function M.get_segment_metrics(self, segment)
-- Esta función devolvería métricas agregadas por segmento
-- En una implementación real, esto vendría de tu backend analytics
return {
segment = segment,
avg_session_length = 0,
avg_ltv = 0,
retention_rate = 0,
conversion_rate = 0
}
end
return M
Custom Events y Funnels
1. Funnel Analysis
Análisis de Embudo
-- funnel_analytics.lua
local M = {}
function init(self)
self.analytics_manager = require "main.analytics_manager"
self.active_funnels = {}
-- Definir funnels importantes
self:setup_funnels()
end
function M.setup_funnels(self)
self.funnels = {
onboarding = {
name = "User Onboarding",
steps = {
"app_open",
"tutorial_start",
"tutorial_complete",
"first_level_start",
"first_level_complete"
}
},
purchase = {
name = "Purchase Flow",
steps = {
"store_open",
"product_view",
"purchase_attempt",
"purchase_complete"
}
},
level_progression = {
name = "Level Progression",
steps = {
"level_start",
"checkpoint_reached",
"level_complete",
"next_level_unlock"
}
},
retention = {
name = "User Retention",
steps = {
"day_1_login",
"day_3_login",
"day_7_login",
"day_14_login",
"day_30_login"
}
}
}
end
function M.track_funnel_step(self, funnel_name, step_name, user_id, additional_data)
local funnel = self.funnels[funnel_name]
if not funnel then
print("Unknown funnel: " .. funnel_name)
return
end
-- Verificar si es un paso válido
local step_index = self:get_step_index(funnel, step_name)
if not step_index then
print("Unknown step in funnel " .. funnel_name .. ": " .. step_name)
return
end
-- Inicializar tracking del usuario si es necesario
if not self.active_funnels[user_id] then
self.active_funnels[user_id] = {}
end
if not self.active_funnels[user_id][funnel_name] then
self.active_funnels[user_id][funnel_name] = {
start_time = socket.gettime(),
completed_steps = {},
current_step = 1
}
end
local user_funnel = self.active_funnels[user_id][funnel_name]
-- Marcar paso como completado
user_funnel.completed_steps[step_name] = socket.gettime()
user_funnel.current_step = math.max(user_funnel.current_step, step_index)
-- Trackear el evento
self.analytics_manager:track_event("funnel_step", {
funnel_name = funnel_name,
step_name = step_name,
step_index = step_index,
user_id = user_id,
time_since_funnel_start = socket.gettime() - user_funnel.start_time,
is_funnel_complete = self:is_funnel_complete(funnel, user_funnel),
additional_data = additional_data or {}
})
-- Verificar si completó el funnel
if self:is_funnel_complete(funnel, user_funnel) then
self:track_funnel_completion(funnel_name, user_id, user_funnel)
end
end
function M.track_funnel_abandonment(self, funnel_name, user_id, last_step)
local user_funnel = self.active_funnels[user_id] and self.active_funnels[user_id][funnel_name]
if not user_funnel then return end
self.analytics_manager:track_event("funnel_abandonment", {
funnel_name = funnel_name,
user_id = user_id,
last_completed_step = last_step,
steps_completed = self:count_completed_steps(user_funnel),
time_in_funnel = socket.gettime() - user_funnel.start_time
})
end
function track_funnel_completion(self, funnel_name, user_id, user_funnel)
local completion_time = socket.gettime() - user_funnel.start_time
self.analytics_manager:track_event("funnel_complete", {
funnel_name = funnel_name,
user_id = user_id,
completion_time = completion_time,
total_steps = #self.funnels[funnel_name].steps
})
-- Limpiar funnel completado
self.active_funnels[user_id][funnel_name] = nil
end
-- Helper functions
function get_step_index(self, funnel, step_name)
for i, step in ipairs(funnel.steps) do
if step == step_name then
return i
end
end
return nil
end
function is_funnel_complete(self, funnel, user_funnel)
return #user_funnel.completed_steps >= #funnel.steps
end
function count_completed_steps(self, user_funnel)
local count = 0
for _ in pairs(user_funnel.completed_steps) do
count = count + 1
end
return count
end
return M
Performance Analytics
1. Technical Performance
Métricas Técnicas
-- performance_analytics.lua
local M = {}
function init(self)
self.analytics_manager = require "main.analytics_manager"
self.performance_data = {
fps_samples = {},
memory_samples = {},
load_times = {},
crash_reports = {}
}
self.sample_interval = 5.0 -- Muestrear cada 5 segundos
self.sample_timer = 0
end
function update(self, dt)
self.sample_timer = self.sample_timer + dt
if self.sample_timer >= self.sample_interval then
self:collect_performance_sample()
self.sample_timer = 0
end
end
function collect_performance_sample(self)
local fps = 1.0 / (dt or 0.016) -- Calcular FPS actual
local memory_usage = self:get_memory_usage()
-- Almacenar muestras
table.insert(self.performance_data.fps_samples, fps)
table.insert(self.performance_data.memory_samples, memory_usage)
-- Mantener solo las últimas 100 muestras
if #self.performance_data.fps_samples > 100 then
table.remove(self.performance_data.fps_samples, 1)
end
if #self.performance_data.memory_samples > 100 then
table.remove(self.performance_data.memory_samples, 1)
end
-- Detectar problemas de performance
if fps < 20 then
self:track_performance_issue("low_fps", {fps = fps, memory = memory_usage})
end
if memory_usage > 500 * 1024 * 1024 then -- 500MB
self:track_performance_issue("high_memory", {fps = fps, memory = memory_usage})
end
end
function M.track_load_time(self, asset_type, load_time, asset_size)
table.insert(self.performance_data.load_times, {
asset_type = asset_type,
load_time = load_time,
asset_size = asset_size,
timestamp = socket.gettime()
})
self.analytics_manager:track_event("asset_load_time", {
asset_type = asset_type,
load_time = load_time,
asset_size = asset_size,
device_model = sys.get_sys_info().device_model
})
end
function track_performance_issue(self, issue_type, details)
self.analytics_manager:track_event("performance_issue", {
issue_type = issue_type,
fps = details.fps,
memory_usage = details.memory,
device_model = sys.get_sys_info().device_model,
device_language = sys.get_sys_info().language,
app_version = sys.get_config("project.version")
})
end
function M.track_crash(self, crash_type, error_message, stack_trace)
table.insert(self.performance_data.crash_reports, {
crash_type = crash_type,
error_message = error_message,
stack_trace = stack_trace,
timestamp = socket.gettime()
})
self.analytics_manager:track_event("app_crash", {
crash_type = crash_type,
error_message = error_message,
device_info = sys.get_sys_info(),
performance_context = self:get_performance_context()
}, true) -- Enviar inmediatamente
end
function M.send_performance_report(self)
local fps_stats = self:calculate_fps_stats()
local memory_stats = self:calculate_memory_stats()
self.analytics_manager:track_event("performance_report", {
fps_average = fps_stats.average,
fps_min = fps_stats.min,
fps_percentile_95 = fps_stats.p95,
memory_average = memory_stats.average,
memory_peak = memory_stats.peak,
load_time_average = self:calculate_average_load_time(),
crash_count = #self.performance_data.crash_reports
})
end
-- Helper functions
function get_memory_usage(self)
local memory_stats = profiler.get_memory_usage()
return memory_stats and memory_stats.total or 0
end
function calculate_fps_stats(self)
if #self.performance_data.fps_samples == 0 then
return {average = 0, min = 0, p95 = 0}
end
local sorted_fps = {}
for _, fps in ipairs(self.performance_data.fps_samples) do
table.insert(sorted_fps, fps)
end
table.sort(sorted_fps)
local sum = 0
for _, fps in ipairs(sorted_fps) do
sum = sum + fps
end
return {
average = sum / #sorted_fps,
min = sorted_fps[1],
p95 = sorted_fps[math.floor(#sorted_fps * 0.95)]
}
end
function calculate_memory_stats(self)
if #self.performance_data.memory_samples == 0 then
return {average = 0, peak = 0}
end
local sum = 0
local peak = 0
for _, memory in ipairs(self.performance_data.memory_samples) do
sum = sum + memory
peak = math.max(peak, memory)
end
return {
average = sum / #self.performance_data.memory_samples,
peak = peak
}
end
function calculate_average_load_time(self)
if #self.performance_data.load_times == 0 then return 0 end
local sum = 0
for _, load_data in ipairs(self.performance_data.load_times) do
sum = sum + load_data.load_time
end
return sum / #self.performance_data.load_times
end
function get_performance_context(self)
return {
current_fps = #self.performance_data.fps_samples > 0 and self.performance_data.fps_samples[#self.performance_data.fps_samples] or 0,
current_memory = #self.performance_data.memory_samples > 0 and self.performance_data.memory_samples[#self.performance_data.memory_samples] or 0
}
end
return M
Mejores Prácticas
1. Data Privacy y GDPR
- Obtener consentimiento explícito para tracking
- Implementar opt-out fácil
- Anonimizar datos personales
- Documentar qué datos se recopilan
- Cumplir con regulaciones locales
2. Performance
- Usar batching para reducir requests
- Implementar retry logic con backoff
- Cache events localmente en caso de network issues
- Limitar frecuencia de eventos
- Optimizar payload size
3. Data Quality
- Validar eventos antes de enviar
- Usar esquemas consistentes
- Implementar sampling para eventos de alto volumen
- Manejar casos edge apropiadamante
- Monitorear data integrity
4. Business Intelligence
- Definir KPIs claros
- Crear dashboards útiles
- Implementar alertas automáticas
- Hacer análisis de cohortes
- Trackear funnel conversion rates
Esta implementación completa te proporciona un sistema robusto de analytics que te ayudará a entender y optimizar el comportamiento de tus usuarios.