Telemetría de actividad de usuario para Windows — captura, persiste y analiza.
Un servicio de bandeja que observa cómo se usa realmente un equipo: qué aplicaciones se abren, cuánto tiempo están en primer plano, cuánto se teclea, dónde se hace clic, qué se copia y qué archivos se tocan. Todo se guarda en SQL Server en un modelo de eventos extensible y se expone con vistas SQL listas para Power BI.
- ¿Qué es SentinelTelemetry?
- Características
- Arquitectura
- Stack tecnológico
- Estructura del repositorio
- Requisitos
- Puesta en marcha
- Catálogo de eventos
- Modelo de datos
- Vistas SQL
- Dashboard Power BI
- Uso en Power BI
- Extender con un nuevo collector
- Privacidad y datos sensibles
- Roadmap
- Contribuir
- Licencia
SentinelTelemetry es una plataforma de telemetría local (single-machine) que responde preguntas como:
- ¿Qué aplicaciones se usan más y por cuánto tiempo están en primer plano?
- ¿Qué tan rápido y cuánto se escribe cada día?
- ¿En qué zonas de la pantalla se concentran los clics?
- ¿Qué carpetas se navegan y qué archivos se crean, renombran o eliminan?
- ¿Cuánto tiempo real de actividad hay frente al tiempo inactivo?
- ¿Qué aplicaciones se abren al iniciar sesión y cómo se salta de una app a otra?
En lugar de escribir una tabla por métrica, el proyecto usa un único modelo de eventos:
una tabla Events con metadatos indexables (source, category, event_type, timestamp) y un
payload JSON (data_json). Añadir una nueva fuente de telemetría no requiere migrar el esquema:
basta con crear un nuevo Collector y su modelo. Las vistas SQL hacen el trabajo de
"desempaquetar" ese JSON en columnas analíticas.
📄 ¿Vas a construir informes? Revisa el documento complementario
DOCUMENTACION_POWERBI.mdcon el modelo en estrella, medidas DAX y consultas de validación.
| Módulo | Qué captura | Cómo |
|---|---|---|
| 🪟 Ventana activa | Aplicación y título de la ventana en primer plano | Sondeo de GetForegroundWindow (solo al cambiar) |
| 🚀 Ciclo de vida de apps | Apertura y cierre de procesos con ventana visible | Sondeo de Process + MainWindowHandle |
| ⌨️ Teclado | Pulsaciones agregadas por segundo | Hook de bajo nivel (WH_KEYBOARD_LL) |
| 🖱️ Ratón | Posición, clic izquierdo/derecho y movimiento | GetCursorPos + GetAsyncKeyState (umbral 4 px) |
| 📋 Portapapeles | Tipo de contenido (Text/Image/FileDrop) y longitud |
AddClipboardFormatListener (WM_CLIPBOARDUPDATE) |
| 🗂️ Explorador | Carpeta que se está navegando | COM ShellWindows |
| 📁 Archivos | Creación, borrado, renombrado y modificación | FileSystemWatcher en carpetas configuradas |
| 💤 Inactividad | Inicio/fin de periodos idle (umbral 30 s) | Máquina de estados sobre actividad de entrada |
| 🔐 Sesión | Login, Logout, Lock, Unlock, RemoteConnect/-Disconnect, Suspend, Resume | Eventos del sistema (SystemEvents) |
| 🧩 Control | Pausar/Reanudar/Cerrar sin cerrar sesión | Icono de bandeja (NotifyIcon) |
| 🛡️ Robustez | Conexión con reintentos + hooks que no bloquean | Reintento exponencial simple y hilos de mensajes dedicados |
flowchart LR
subgraph WIN["🖥️ Equipo Windows del usuario"]
direction TB
subgraph HOOKS["Hooks / eventos del sistema"]
KH["KeyboardHook<br/>WH_KEYBOARD_LL"]
CH["ClipboardHook<br/>WM_CLIPBOARDUPDATE"]
SC["SessionCollector<br/>SessionSwitch"]
FW["FileCollector<br/>FileSystemWatcher"]
end
subgraph POLL["Collectors por sondeo (1 s)"]
WC["WindowCollector"]
AC["ApplicationCollector"]
PC["PointerCollector"]
KC["KeyboardCollector"]
EC["ExplorerCollector"]
IC["IdleCollector"]
end
WK["⚙️ Worker<br/>BackgroundService"]
PC2["⏸️ PauseController"]
REPO["🗄️ SqlServerEventRepository"]
HOOKS --> WK
POLL --> WK
WK --> PC2
WK --> REPO
end
DB[("SQL Server<br/>SentinelTelemetryDb")]
REPO --> DB
DB --> VIEWS["📊 Vistas SQL<br/>vw_*"]
VIEWS --> PBI["📈 Power BI"]
Flujo de datos
- Los collectors producen objetos
EventBase(conSource,Category,EventType,Version). - El Worker los recoge en un ciclo de 1 segundo, aplica filtros de ruido y los persiste.
- El repositorio resuelve/crea las claves de catálogo y guarda el evento serializado a JSON.
- Las vistas convierten el JSON en tablas analíticas.
- Power BI consume directamente esas vistas.
- Lenguaje: C# 14 · Runtime: .NET 10 (
net10.0,net10.0-windows) - Host del servicio:
Microsoft.Extensions.Hosting(Generic Host +BackgroundService) - Persistencia:
Microsoft.Data.SqlClientsobre SQL Server - Interop Windows: P/Invoke a
user32/kernel32+ COM (Shell32,SHDocVw) - UI de bandeja: Windows Forms (
NotifyIcon); proyecto WPF reservado para una UI futura - Analítica: T-SQL (vistas) → Power BI
SentinelTelemetry/
├── SentinelTelemetry.slnx # Solución (formato slnx)
├── README.md # Este archivo
├── DOCUMENTACION_POWERBI.md # Contexto técnico y de datos
├── PowerBI/
│ ├── SentinelTelemetryReport.pbix # Informe: 4 páginas, 36 medidas
│ └── GUIA_MEDIDAS_DAX.md # Paso a paso de DAX y páginas
│
├── SentinelTelemetry.Core/ # 📦 Modelos e interfaces (net10.0)
│ ├── Models/
│ │ ├── EventBase.cs # Clase base de todos los eventos
│ │ ├── WindowEvent.cs ApplicationEvent.cs PointerEvent.cs
│ │ ├── KeyboardEvent.cs ClipboardEvent.cs ExplorerEvent.cs
│ │ ├── FileEvent.cs IdleEvent.cs SessionEvent.cs
│ └── Interfaces/
│ ├── IEventRepository.cs # SaveEventAsync(EventBase)
│ └── IEventCollector.cs # Capture() -> EventBase
│
├── SentinelTelemetry.Agent/ # 🎛️ Collectors (Telemetry.csproj)
│ ├── WindowCollector.cs ApplicationCollector.cs PointerCollector.cs
│ ├── KeyboardCollector.cs KeyboardHook.cs ClipboardCollector.cs
│ ├── ClipboardHook.cs ExplorerCollector.cs FileCollector.cs
│ ├── IdleCollector.cs SessionCollector.cs NativeMethods.cs
│
├── SentinelTelemetry.Storage/ # 🗄️ Persistencia (Persistence.csproj)
│ └── SqlServerEventRepository.cs # Crea la BD/tablas e inserta eventos
│
├── SentinelTelemetry.Service/ # ⚙️ Servicio Windows (WinExe)
│ ├── Program.cs # Host + bandeja + reintentos de conexión
│ ├── Worker.cs # Bucle de captura (1 s)
│ ├── PauseController.cs # Pausa/Reanudación
│ └── appsettings.json # Cadena de conexión + carpetas vigiladas
│
├── SentinelTelemetry.UI/ # 🖥️ UI WPF (placeholder)
│ └── MainWindow.xaml
│
└── SentinelTelemetry.Database/ # 🧱 Scripts T-SQL
├── 01_Schema_Creation.sql
├── 02_vw_pointer_unpacked_data.sql
├── 03_vw_window_activity_duration.sql
├── ...
├── 15_vw_transitions_between_apps.sql
└── 16_index_events_idle.sql
ℹ️ Nota: algunos nombres de carpeta y de
.csprojno coinciden (SentinelTelemetry.Agent→SentinelTelemetry.Telemetry.csproj,SentinelTelemetry.Storage→SentinelTelemetry.Persistence.csproj,SentinelTelemetry.UI→SentinelTelemetry.UI.WPF.csproj).
| Requisito | Detalle |
|---|---|
| SO | Windows 10/11 (usa hooks e interop nativa). |
| SDK | .NET 10 SDK. |
| BD | SQL Server (LocalDB, Express o superior) accesible desde el equipo. |
| Permisos | El usuario debe poder ejecutar hooks globales de entrada. |
⚠️ El esquema autoritativo lo crea la aplicación (SqlServerEventRepository.EnsureDatabaseAndTableCreated()). El script01_Schema_Creation.sqles una referencia manual y difiere en detalles menores (no incluye las restriccionesUNIQUE). Confía en el código si quieres el esquema exacto.
git clone https://github.com/jordihuaman/SentinelTelemetry.git
cd SentinelTelemetry
dotnet build SentinelTelemetry.slnxEdita SentinelTelemetry.Service/appsettings.json:
dotnet run --project SentinelTelemetry.Service- El servicio crea la base de datos y las tablas automáticamente si no existen.
- Verás un icono en la bandeja del sistema con el menú Pausar / Reanudar / Cerrar.
- La captura comienza de inmediato (con un ciclo inicial que fija el estado sin emitir eventos).
USE SentinelTelemetryDb;
-- Volumen por origen y tipo (últimos 7 días)
SELECT S.source_name, E.event_type, COUNT(*) AS Eventos
FROM dbo.Events E
JOIN dbo.Source S ON S.id_source = E.id_source
WHERE E.timestamp >= DATEADD(DAY, -7, SYSUTCDATETIME())
GROUP BY S.source_name, E.event_type
ORDER BY Eventos DESC;
-- Top aplicaciones por tiempo en primer plano
SELECT TOP 10 Process_Name AS Aplicacion,
ROUND(SUM(Duration_Seconds) / 3600.0, 2) AS Horas
FROM vw_window_activity_duration
WHERE Duration_Seconds IS NOT NULL
GROUP BY Process_Name
ORDER BY Horas DESC;Combinaciones que produce el sistema. El JSON guarda las propiedades específicas en
PascalCase (además de Id, Timestamp, Source, Category, EventType, Version).
Source |
Category |
event_type |
Payload JSON |
|---|---|---|---|
Window |
System |
ActiveWindowSnapshot |
ProcessName, WindowTitle |
Application |
Lifecycle |
Opened, Closed |
ProcessName, ProcessId, WindowTitle |
Pointer |
Input |
MouseMove, LeftClick, RightClick |
X, Y, LeftClick, RightClick |
Keyboard |
Input |
KeyboardSnapshot |
KeyPressCount |
ClipboardCollector |
Clipboard |
ClipboardChanged |
ContentType, ContentLength |
ExplorerCollector |
Explorer |
FolderOpened, FolderChanged, FolderClosed |
FolderPath |
FileCollector |
File |
Created, Deleted, Renamed, Modified |
FilePath, OldFilePath |
Idle |
Attention |
IdleStarted, IdleEnded |
— |
Session |
System |
Login, Logout, Lock, Unlock, RemoteConnect, RemoteDisconnect, Suspend, Resume |
— |
Reglas de muestreo (clave para interpretar los datos):
- ⏱️ El
Workercicla cada 1 segundo. - 🪟 La ventana se guarda solo cuando cambia (
ProcessNameoWindowTitle). - ⌨️ El teclado solo se guarda si
KeyPressCount > 0(segundos con tecleo). - 🖱️ El ratón solo se guarda con clic o movimiento ≥ 4 px.
- 💤 El umbral de inactividad es 30 segundos sin entrada.
- 🚀/🗂️ App y Explorador no emiten en el primer ciclo (fijan la "foto" inicial).
Modelo event-sourcing ligero: una tabla de hechos + tres catálogos normalizados.
erDiagram
Source ||--o{ Events : "id_source"
Category ||--o{ Events : "id_category"
Version ||--o{ Events : "id_version"
Events {
uniqueidentifier id_event PK
datetime2 timestamp "UTC"
int id_source FK
int id_category FK
int id_version FK
nvarchar event_type
nvarchar data_json "payload JSON"
}
Source {
int id_source PK
nvarchar source_name "UNIQUE"
nvarchar description
}
Category {
int id_category PK
nvarchar category_name "UNIQUE"
nvarchar description
}
Version {
int id_version PK
int numero_version "UNIQUE"
nvarchar description
}
| Tabla | Rol | Notas |
|---|---|---|
Events |
Hecho | timestamp es UTC (DateTime.UtcNow). |
Source |
Dimensión | Origen/collector (se auto-crea). |
Category |
Dimensión | Categoría funcional (se auto-crea). |
Version |
Dimensión | Versión del esquema del evento (hoy siempre 1). |
Ejemplo de data_json:
{
"ProcessName": "Code",
"WindowTitle": "Worker.cs - SentinelTelemetry",
"Id": "3f2b1c9e-...",
"Timestamp": "2026-05-01T14:23:11.1234567Z",
"Source": "Window",
"Category": "System",
"EventType": "ActiveWindowSnapshot",
"Version": 1
}| Vista | Origen | Columnas de negocio |
|---|---|---|
vw_pointer_unpacked_data |
Pointer |
PosicionX, PosicionY, ClicIzquierdo, ClickDerecho |
vw_window_unpacked_data |
Window |
Aplicacion, Ventana_Interna |
vw_keyboard_unpacked_data |
Keyboard |
Pulsaciones |
vw_clipboard_unpacked_data |
ClipboardCollector |
Tipo_Contenido, Longitud_Contenido |
vw_applicationCycle_unpacked_data |
Application |
Programa |
vw_explorer_unpacked_data |
ExplorerCollector |
Ruta |
vw_fileManagement_unpacked_data |
FileCollector |
RutaAnterior, RutaActual |
| Vista | Métrica que produce |
|---|---|
vw_window_activity_duration |
Start_Time, End_Time, Duration_Seconds por app (vía LEAD). |
vw_keyboard_daily_summary |
Pulsaciones por día (hora Perú, UTC-5). |
vw_keyboard_typing_metrics |
Sesiones de tecleo con KPS y KPM (gap ≥ 2 s). |
vw_pointer_heatmap |
Mapa de calor en bins de 10×10 px. |
vw_idle_activity_periods |
Periodos Idle y Activo con duración. |
vw_apps_when_turnedOn_metrics |
Apps abiertas tras Login/Unlock y % de sesiones. |
vw_transitions_between_apps |
Matriz de transiciones entre apps y % de salidas. |
El informe está en PowerBI/SentinelTelemetryReport.pbix
y consume las vistas directamente, sin ETL intermedio: 4 páginas visibles, 36 medidas DAX y una
tabla _Medidas aislada para no mezclar medidas con columnas.
Periodo: 23/07/2026 – 16/10/2026 (86 días) · 281.309 eventos reales.
| Página | Pregunta que responde | Visuales principales |
|---|---|---|
| Resumen Diario | ¿Cómo se reparte el día? | Tarjetas, línea activo/inactivo, anillo de apps, tabla por día |
| Uso de Aplicaciones | ¿Qué apps uso y cuánto? | Top 10 por tiempo, tabla con % del total, dispersión apps vs eventos |
| Actividad e Inactividad | ¿Cuánto trabajo real frente a idle? | Tarjetas activo/inactivo, línea diaria, anillo 69/31, % por día de la semana |
| Transiciones | ¿A dónde salto desde cada app? | Top 10 de pares, matriz 10×10 con mapa de calor |
| Métrica | Valor |
|---|---|
| Eventos capturados | 281.309 |
| Pulsaciones de teclado | 373.104 |
| Tiempo activo | 1.381,31 h (69,4%) |
| Tiempo inactivo | 609,09 h (30,6%) |
| Apps distintas usadas | 115 |
| Transiciones entre apps | 16.662 en 833 pares |
- 69,4% del tiempo fue actividad real. El resto son pausas, bloqueos y noches.
- Sábado es el día más activo (85,6%) y domingo el menos (44,4%).
exploreres el hub: 4.064 salidas hacia 88 destinos distintos. El par más frecuente esexplorer → chrome(719 veces).SSMS → braveconcentra el 41,9% de las salidas de SSMS, aunque en volumen absoluto ganeexplorer → chrome. Son dos preguntas distintas: hábito de una app vs tráfico total.
vw_idle_activity_periodsduplicaba la línea de tiempo: emparejaba cadaIdleStartedcon el siguienteIdleStarted(y cadaIdleEndedcon el siguienteIdleEnded). Se corrigió con un soloLEADy el índice16_index_events_idle.sql. AhoraActivo + Idle = 1.990,4 hexactas.- Las fechas se convierten de UTC a hora Perú en DAX (
[timestamp] - TIME(5,0,0)), no en SQL, yDimFechaestá marcada como tabla de fechas conMesordenado porMesNum. vw_window_activity_durationtodavía aplica un tope de 30 minutos por evento. La página de apps usaTiempo en App (h) con tope; queda pendiente la duración real por ventana.- El PBIX guarda una copia del periodo. Para Actualizar necesitas la base local con los datos.
👉 Paso a paso de las 36 medidas, fórmulas y trampas:
PowerBI/GUIA_MEDIDAS_DAX.md.
- Obtener datos → SQL Server → tu instancia (por ejemplo
.\SQLEXPRESS), baseSentinelTelemetryDb, autenticación Windows. - Modelo del informe: las vistas actúan como tablas de hechos (ventana, idle, transiciones,
teclado, puntero) +
DimFecha+_Medidas. Los catálogosSource/Category/Versionse quedan solo en la base. - Zona horaria:
timestampes UTC. Las vistas de teclado ya traen UTC-5; en el informe el resto se convierte con columnas calculadas de DAX yDimFecha. - Limpieza:
ClicIzquierdo/ClickDerechollegan como texto"true"/"false"; conviértelos a booleano.Longitud_Contenidoa entero.
Pulsaciones Totales = SUM ( vw_keyboard_unpacked_data[Pulsaciones] )
Tiempo Activo (h) =
VAR Segundos = SUMX ( vw_idle_activity_periods,
IF ( vw_idle_activity_periods[TipoPeriodo] = "Activo",
vw_idle_activity_periods[DuracionSegundos] ) )
RETURN DIVIDE ( Segundos, 3600 )
KPM Promedio = AVERAGE ( vw_keyboard_typing_metrics[KPM] )
👉 Detalle completo (modelo, medidas, páginas y gotchas) en
DOCUMENTACION_POWERBI.md.
El modelo está pensado para crecer sin tocar el esquema. Ejemplo: añadir telemetría de batería.
// 1) Modelo (SentinelTelemetry.Core/Models)
public class BatteryEvent : EventBase
{
public int Percent { get; set; }
public bool IsCharging { get; set; }
}
// 2) Collector (SentinelTelemetry.Agent)
public class BatteryCollector : IEventCollector
{
public EventBase Capture() => new BatteryEvent
{
Source = "Battery",
Category = "System",
EventType = "BatterySnapshot",
Percent = 87,
IsCharging = true
};
}- Invócalo desde
Worker.ExecuteAsyncy persístelo con_repository.SaveEventAsync(...). - Crea una vista que exponga su payload:
CREATE VIEW vw_battery_unpacked_data AS
SELECT E.id_event, E.[timestamp], E.event_type,
CAST(JSON_VALUE(E.data_json, '$.Percent') AS INT) AS Porcentaje,
JSON_VALUE(E.data_json, '$.IsCharging') AS Cargando
FROM dbo.Events E
JOIN dbo.Source S ON S.id_source = E.id_source
WHERE S.source_name = 'Battery';- Añade la vista al modelo de Power BI. No hace falta migrar nada.
Este proyecto captura información sensible del equipo donde se ejecuta: títulos de ventana, rutas de archivos, tipos de contenido del portapapeles y patrones de actividad.
- 🔒 Úsalo solo en equipos propios o con consentimiento explícito del usuario.
- 📌 Los datos se guardan localmente en tu instancia de SQL Server; no se envían a ningún servicio externo.
- 🧹 El contenido del portapapeles no se almacena: solo su tipo y longitud.
- ⚖️ Cumple la normativa aplicable (RGPD/leyes locales) si lo despliegas en terceros.
- Modelo de eventos genérico y persistencia en SQL Server.
- Collectors de ventana, apps, teclado, ratón, portapapeles, explorador, archivos, idle y sesión.
- Vistas SQL de desempaquetado y métricas.
- Control por bandeja (pausar/reanudar/cerrar).
- Informe Power BI con 4 páginas y 36 medidas sobre las vistas.
- UI WPF para exploración en vivo (proyecto ya reservado).
- Migraciones/esquema versionado con scripts idempotentes.
- Soporte multi-equipo (dimensión
Device). - Empaquetado como instalador/servicio de Windows.
- Empaquetar el informe como plantilla (
.pbit) reutilizable.
- Haz un fork del repositorio.
- Crea una rama:
git checkout -b feature/mi-mejora. - Sigue el estilo existente (nombres en inglés para el código, comentarios en español).
- Añade/actualiza las vistas SQL si tocas el modelo de eventos.
- Abre un Pull Request describiendo el cambio.
Este proyecto se distribuye bajo la licencia MIT. Puedes usarlo, copiarlo, modificarlo, fusionarlo, publicarlo, distribuirlo, sublicenciarlo y vender copias, siempre que conserves el aviso de copyright y el texto de la licencia. Se entrega “tal cual”, sin garantías.
El texto completo está en LICENSE.
Hecho con 🧠 y muchos P/Invoke · SentinelTelemetry
{ "ConnectionStrings": { "SentinelDb": "Server=.;Database=SentinelTelemetryDb;Trusted_Connection=True;TrustServerCertificate=True;" }, "FileWatcher": { "WatchedFolders": [ "C:\\Users\\<usuario>\\Desktop", "C:\\Users\\<usuario>\\Documents", "C:\\Users\\<usuario>\\Downloads" ], "ExcludedFolderNames": [ "node_modules", ".git", "bin", "obj", ".vs" ] } }