← Volver al listado de tecnologías

Analytics, Eventos y Tracking de Usuarios

Por: Artiko
defoldanalyticstrackingeventosusuariosmétricas

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

2. Performance

3. Data Quality

4. Business Intelligence

Esta implementación completa te proporciona un sistema robusto de analytics que te ayudará a entender y optimizar el comportamiento de tus usuarios.