Is ggsel Ханти: 1080p на Twitch, игры дешевле и Steam в рублях safe?
The extension sends Twitch browsing activity and a persistent user ID to both the operator's backend and Yandex Metrica on every channel visit.
Each time a user visits a Twitch channel, the extension forwards the channel URL, game title, stream session details, and a stable cross-session UUID to twitch.ggsel.net and to Yandex Metrica (mc.yandex.ru). The persistent user ID is generated on first install and stored in chrome.storage.local, allowing the operator to track the same user across sessions. The extension also polls the operator backend every three minutes and displays push notifications with operator-controlled titles, bodies, and click URLs.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Twitch activity sent to ggsel.net events backend
The extension POSTed an event batch to twitch.ggsel.net/api/v1/events.
Events come from Twitch page visits, popup opens, activity markers, and stream sessions, tagged with a user ID, session ID, extension version, and Twitch context.
You browse Twitch or open the extension popup while the extension is installed.
The content script reads the current Twitch page, channel name, and stream-session state from the page.
The extension creates analytics events and sends them to the ggsel.net events backend.
Those events include persistent user and session identifiers that connect multiple actions to the same browser profile.
| content-type | application/json |
| Field | Value | Why it matters | |
|---|---|---|---|
Persistent user ID | ac443594-8a1f-4d3b-9f4c-2b1d6e7c8a90 | Lets the backend connect events from the same browser profile across separate browsing sessions. | |
Browser-session ID | 74ed2e90-2a6d-46c1-9f9d-1e8f7c5b4a32 | Groups activity that happened during the same browser session. | |
Extension version | 1.0.17.1 | Shows which installed version produced the event. | |
Twitch page location | https://www.twitch.tv/shroud | Shows which Twitch page or channel was open when the extension created the event. | |
Channel and game context | channel_name=shroud, game_title=VALORANT | Describes the stream channel and game associated with the visit or stream session. | |
Stream duration | duration_sec=1840 | Records how long a stream session lasted before the tab closed, changed, or became idle. |
Source code path that creates Twitch events and posts them to ggsel.net
export const DEFAULT_API_BASE = 'https://twitch.ggsel.net';
export const REQUEST_TIMEOUT_MS = 7000;
// Canonical source marker for links opened from the Twitch extension.
export const DEFAULT_UTM_SOURCE = 'ggsel_twitch_extension';
// Stable canonical UTM medium for the extension (spec 3.3)
export const DEFAULT_UTM_MEDIUM = 'twitch_app';
export const GA_MEASUREMENT_ID = 'G-NCQMPNWKWS';
export const GA_API_SECRET = 'grdZVJk4ROmZPlW9HA_CpQ';
export const YM_COUNTER_ID = '104649646';
// Phase 1 own analytics — third sink parallel to GA4/Yandex.
// Set to false as kill switch if backend is overloaded or under attack.
export const OWN_ANALYTICS_ENABLED = true;
export const OWN_ANALYTICS_TIMEOUT_MS = 5000;
export function buildEventsEndpoint(base = DEFAULT_API_BASE) {
return new URL('/api/v1/events', base).toString();
} function trackVisit(reason) {
try {
const pageUrl = getSafePageLocation();
usageTracker?.notifyContextChange(buildUsageContext(), reason || 'visit');
if (pageUrl === lastVisitLocation) return;
lastVisitLocation = pageUrl;
const channelName = getChannelName();
sendAnalyticsEvent('twitch_visit', {
visit_reason: reason,
channel_name: channelName || undefined,
page_location: pageUrl,
page_path: location.pathname,
});
touchMonthlyActive('twitch_visit', {
visit_reason: reason,
channel_name: channelName || undefined,
});
maybeStartStreamSession(channelName, reason);
} catch (error) {
console.warn('Failed to track visit', error);
}
}
function maybeStartStreamSession(channelName, reason) {
// Switch / leave channel → end the current session before starting a new one
if (activeStreamSession && activeStreamSession.channelName !== channelName) {
endStreamSession('navigate');
}
if (streamSessionStartTimer) {
clearTimeout(streamSessionStartTimer);
streamSessionStartTimer = null;
}
if (!channelName) return;
if (activeStreamSession && activeStreamSession.channelName === channelName) return;
streamSessionStartTimer = setTimeout(() => {
streamSessionStartTimer = null;
// Re-check the channel hasn't changed during the wait.
if (getChannelName() !== channelName) return;
const gameTitle = getGameTitle();
activeStreamSession = {
sessionId: generateLocalId(),
startedAt: Date.now(),
channelName,
gameTitle,
};
sendAnalyticsEvent('stream_session_start', {
stream_session_id: activeStreamSession.sessionId,
channel_name: channelName,
game_title: gameTitle || undefined,
visit_reason: reason || 'load',
page_location: getSafePageLocation(),
page_path: location.pathname,
});
}, STREAM_SESSION_MIN_MS);
}
function endStreamSession(endReason) {
if (!activeStreamSession) return;
const { sessionId, startedAt, channelName, gameTitle } = activeStreamSession;
activeStreamSession = null;
const durationMs = Date.now() - startedAt;
// Re-read game_title at session end — streamer may have switched games.
// Fall back to the value captured at start so we always have something.
const finalGameTitle = getGameTitle() || gameTitle || null;
sendAnalyticsEvent('stream_session_end', {
stream_session_id: sessionId,
channel_name: channelName,
game_title: finalGameTitle || undefined,
duration_ms: durationMs,
duration_sec: Math.round(durationMs / 1000),
end_reason: endReason || 'unknown',
});
}export async function buildOwnEvent(eventName, params, state) {
const userId = (state && (state.userId || state.clientId)) || '';
const sessionId = await getSessionId();
return {
event_id: generateId(),
name: eventName,
ts: Date.now(),
user_id: userId,
session_id: sessionId,
client_id: (state && state.ggselClientId) || null,
properties: pickAllowedProperties(params),
extension_version: getManifestVersion() || null,
};
}
// When a batch buffer is configured (by background.js), events are queued
// instead of sent immediately. Direct send remains the fallback path for
// non-service-worker contexts.
let ownBackendBuffer = null;
export function setOwnBackendBuffer(buffer) {
ownBackendBuffer = buffer && typeof buffer.push === 'function' ? buffer : null;
}
// Spec section 6.3: flush queue immediately when popup opens (user is
// actively engaging — minimize buffering latency for these touchpoints).
const FLUSH_AFTER_EVENTS = new Set([
'popup_open',
'extension_install',
'extension_update',
'ggsel_button_click',
]);
async function sendToOwnBackend(eventName, params, state) {
if (!OWN_ANALYTICS_ENABLED) return false;
if (!OWN_BACKEND_KNOWN_EVENTS.has(eventName)) return false;
if (!state || (!state.userId && !state.clientId)) return false;
const event = await buildOwnEvent(eventName, params, state);
if (!event.user_id) return false;
if (ownBackendBuffer) {
try {
await ownBackendBuffer.push(event);
if (FLUSH_AFTER_EVENTS.has(eventName)) {
// fire-and-forget — flush errors stay inside the buffer
ownBackendBuffer.flush().catch(() => {});
}
return true;
} catch (error) {
console.warn('Own analytics buffer push failed; falling back to direct send', error);
}
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), OWN_ANALYTICS_TIMEOUT_MS);
try {
const response = await fetch(buildEventsEndpoint(), {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ events: [event] }),
credentials: 'omit',
signal: controller.signal,
});
return response.ok;
} catch (error) {
console.warn('Own analytics send failed', error);
return false;
} finally {
clearTimeout(timer);
}
}async function sendOwnAnalyticsBatch(events) {
if (!Array.isArray(events) || events.length === 0) return;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), OWN_ANALYTICS_TIMEOUT_MS);
try {
const response = await fetch(buildEventsEndpoint(), {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ events }),
credentials: 'omit',
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`analytics batch HTTP ${response.status}`);
}
} finally {
clearTimeout(timer);
}
}- twitch.ggsel.net
Receives the extension-owned JSON event batches for Twitch visits, popup opens, activity markers, and stream sessions.