Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/botnadzor/extension/llms.txt

Use this file to discover all available pages before exploring further.

The Reporting feature allows you to submit suspicious accounts to Botnadzor administrators for review. Reports help improve the bot detection database and protect the VK community.

How to Report an Account

  1. Open the Account Inspector for the suspicious account
  2. Switch to the Report tab
  3. Fill out the report form with:
    • Tag suggestion (bot category)
    • Detailed description of suspicious behavior
    • Trigger information (where you found the account)
  4. Submit the report

CollectingService

The extension includes a sophisticated system for collecting comment data to help with bot detection. This service runs in the background and is managed by CollectingService (src/entrypoints/background/@services/collecting-service.ts).

Automatic Comment Collection

When enabled, the extension automatically collects metadata about comments you encounter:
type CommentToCollect = {
  wallVkId: VkId;           // Wall owner ID (e.g., wall-123_456 -> -123)
  postVkId: PositiveVkId;   // Post ID (e.g., wall-123_456 -> 456)
  commentVkId: PositiveVkId; // Comment ID (e.g., ?reply=789 -> 789)
  commenterVkDomain: VkDomain; // Commenter's VK domain
  postCommentCount: number | undefined; // Total comments on post
};

Collection Flow

async collectCommentIfNeeded(
  commentToCollect: CommentToCollect,
): Promise<void> {
  const [userOptedIn, wallSkipped] = await Promise.all([
    this.checkIfUserOptedIn(),
    this.checkIfWallSkipped(commentToCollect.wallVkId),
  ]);
  
  if (!userOptedIn) {
    logger.debug(
      "Comment {commentSlug} was ignored: user not opted in",
      { commentSlug },
    );
    return;
  }
  
  if (wallSkipped) {
    logger.debug(
      "Comment {commentSlug} was ignored: wall skipped",
      { commentSlug },
    );
    return;
  }
  
  this.notYetPersistedComments.push(commentToCollect);
  this.scheduleNextPersist();
}
Collection only happens when:
  • User has opted into comment collection in settings
  • User has valid authentication
  • Wall is not in the skip list
  • Page is not an archived snapshot

Opt-In Required

Comment collection is entirely opt-in:
private async doCheckIfUserOptedIn(): Promise<boolean | "maybe"> {
  const authStatus = this.authService.getAuthStatus();
  
  if (authStatus.state === "empty") {
    return false;
  }
  
  const userConfig = await this.userConfigService.get();
  if (!userConfig.collectingComments) {
    return false;
  }
  
  if (authStatus.state === "valid") {
    return true;
  }
  
  return false;
}
To enable:
  1. Configure authentication in extension settings
  2. Enable “Collecting Comments” option
  3. Extension will automatically start collecting data

Data Storage and Upload

IndexedDB Storage

Collected comments are stored locally in IndexedDB:
this.db = new Dexie("collecting");
this.db.version(1).stores({
  [commentsTableName]: "[wallVkId+postVkId+commentVkId], persistedAt, uploadedAt",
  [postsTableName]: "[wallVkId+postVkId]",
});
Storage schema:
type PersistedComment = {
  wallVkId: VkId;
  postVkId: PositiveVkId;
  commentVkId: PositiveVkId;
  commenterVkIdOrNickname: PositiveVkId | VkNickname;
  persistedAt: IsoDateTime;
  uploadedAt: IsoDateTime | "-";
};

type PersistedPost = {
  wallVkId: VkId;
  postVkId: PositiveVkId;
  postCommentCount: number;
  persistedAt: IsoDateTime;
};

Persistence Throttling

Comments are batched and persisted periodically:
const persistingThrottleInterval = 5000; // 5 seconds

private scheduleNextPersist(): void {
  if (this.persistingThrottleTimeout) {
    return; // Already scheduled
  }
  this.persistingThrottleTimeout = setTimeout(() => {
    this.persistingThrottleTimeout = undefined;
    void this.persistRegisteredCommentsIfNeeded();
  }, persistingThrottleInterval);
}
This ensures:
  • Minimal performance impact
  • Batch processing for efficiency
  • Automatic deduplication

Automatic Upload

Stored comments are automatically uploaded to Botnadzor servers:
const uploadingThrottleInterval = 5 * 60 * 1000; // 5 minutes
const uploadedCommentsMinCount = 5;
const uploadedCommentsMaxCount = 1000;

async uploadPersistedCommentsIfNeeded(): Promise<void> {
  const commentsToUpload = rawNotUploadedComments
    .map((rawItem) => this.parsePersistedComment(rawItem))
    .filter((item): item is PersistedComment => item !== undefined)
    .toSorted((itemA, itemB) =>
      itemA.persistedAt.localeCompare(itemB.persistedAt),
    )
    .slice(0, uploadedCommentsMaxCount);
  
  if (commentsToUpload.length < uploadedCommentsMinCount) {
    logger.debug(
      "Not enough comments to upload ({count} < {min}), skipping",
      { count: commentsToUpload.length, min: uploadedCommentsMinCount },
    );
    return;
  }
  
  const outcome = await this.authService.fetchFromDynamicApiWithAccessCode(
    "collect",
    { groupedComments },
  );
}
Upload behavior:
  • Runs every 5 minutes
  • Requires minimum 5 comments
  • Uploads up to 1000 comments per batch
  • Groups comments by post for efficiency
  • Marks uploaded comments to prevent duplicates

Grouped Comment Upload

Comments are grouped by post before upload:
const groupedComments: Array<{
  wallVkId: VkId;
  postVkId: PositiveVkId;
  commentCount?: number;
  comments: Array<{
    commentVkId: PositiveVkId;
    commenterVkIdOrNickname: PositiveVkId | VkNickname;
  }>;
}> = [];
This structure:
  • Reduces API payload size
  • Provides context (total comment count on post)
  • Helps identify bot activity patterns
  • Enables better analysis by administrators

Manual Reporting via Inspector

You can manually report accounts using the Inspector:
// src/entrypoints/background/@services/inspector-service.ts
async submitReport(payload: {
  tagSuggestion: TagSuggestion;
  text: string;
  trigger: InspectorTrigger;
  vkDomain: VkDomain;
}): Promise<ResultOfReportAccount> {
  const { vkDomain, tagSuggestion, text, trigger } = payload;
  
  if (text.length < reportTextMinLength) {
    return {
      problem: true,
      type: "bn:ext:invalid-payload",
      description: `Минимальная длина текста: ${reportTextMinLength} символов`,
      fields: ["text"],
    };
  }
  
  if (text.length > reportTextMaxLength) {
    return {
      problem: true,
      type: "bn:ext:invalid-payload",
      description: `Максимальная длина текста: ${reportTextMaxLength} символов`,
      fields: ["text"],
    };
  }
  
  const vkId = await this.vkDomainResolver.resolve(vkDomain);
  if (!vkId) {
    return {
      problem: true,
      type: "bn:ext:unforeseen-error",
      description: "Не получилось получить ID аккаунта",
    };
  }
  
  const outcome = await this.authService.fetchFromDynamicApiWithAccessCode(
    "reportAccount",
    { tagSuggestion, text, trigger, vkId },
  );
  
  return outcome;
}

Report Requirements

Tag Suggestion: Category for the bot (e.g., spam, propaganda, fake account) Text Description:
  • Minimum length: Defined by reportTextMinLength
  • Maximum length: Defined by reportTextMaxLength
  • Should describe suspicious behavior patterns
  • Should include relevant context
Trigger: How you discovered the account
  • Type: “comment” for comment-based discovery
  • Includes post/comment identifiers for reference

Report Result

type ResultOfReportAccount =
  | { problem: false; /* success */ }
  | { problem: true; type: string; description: string; fields?: string[]; };
Success: Report submitted successfully Error Types:
  • bn:ext:invalid-payload - Invalid report text length or missing data
  • bn:ext:unforeseen-error - Failed to resolve account ID
  • bn:ext:invalid-access-code - Authentication failed
  • bn:ext:missing-permission - Insufficient permissions

Data Pruning

The service automatically manages storage:
const persistedCommentMaxCount = 10_000;

private async prunePersistedDataIfNeeded(): Promise<void> {
  const persistedCommentCount = await commentsTable.count();
  
  if (persistedCommentCount <= persistedCommentMaxCount) {
    return;
  }
  
  const countToDelete = persistedCommentCount - persistedCommentMaxCount;
  
  // Delete oldest comments
  const commentsToDelete = await commentsTable
    .orderBy("persistedAt")
    .limit(countToDelete)
    .toArray();
  
  await commentsTable.bulkDelete(bulkDeleteCommentKeys);
}
Pruning behavior:
  • Maximum 10,000 comments stored
  • Deletes oldest comments first
  • Removes associated post data
  • Cleans up data for skipped walls
  • Runs after each upload cycle

Wall Skip List

Some walls are excluded from collection:
private async doCheckIfWallSkipped(wallVkId: VkId): Promise<boolean> {
  const wall = await this.staticListsService.findItem(
    "walls",
    "vkId",
    wallVkId,
  );
  return wall?.skip ?? false;
}
Skip list features:
  • Cached (LRU cache, max 1000 entries)
  • Synchronized with Botnadzor server
  • Prevents collection from private/sensitive walls
  • Automatically updated

Privacy and Data Control

User Control

You have complete control over data collection:
private resetWithDebounce(): void {
  if (this.resettingDebounceTimeout) {
    clearTimeout(this.resettingDebounceTimeout);
  }
  this.resettingDebounceTimeout = setTimeout(
    () => void this.resetIfNeeded(),
    resetDebounceTimeout, // 10 seconds
  );
}

async resetIfNeeded(): Promise<void> {
  const userOptedIn = await this.checkIfUserOptedIn();
  if (userOptedIn === true || userOptedIn === "maybe") {
    return;
  }
  
  // Clear all data
  await this.getCommentsTable().clear();
  await this.getPostsTable().clear();
  this.notYetPersistedComments = [];
}
When you opt out:
  • Service waits 10 seconds (in case of accidental toggle)
  • Deletes all stored comment data
  • Clears pending uploads
  • Stops collecting new data
  • You can opt back in anytime

What Data is Collected

Collected:
  • VK post/comment identifiers (wall/post/comment IDs)
  • Commenter VK domain (ID or nickname)
  • Post comment count
  • Timestamp of collection
NOT Collected:
  • Comment content/text
  • Your identity as the collector
  • Comment context or replies
  • Private messages
  • Personal information

Data Purpose

Collected data helps Botnadzor:
  • Identify coordinated bot activity
  • Detect spam patterns
  • Build better bot detection models
  • Track bot network evolution
  • Improve community protection

Best Practices

When Reporting Accounts

  1. Gather evidence - Use Inspector to analyze behavior first
  2. Check registration date - New accounts with high activity are suspicious
  3. Be specific - Describe exact suspicious patterns observed
  4. Include context - Where did you find the account? What were they doing?
  5. Choose appropriate tag - Select the category that best fits the behavior

Writing Good Reports

Good report example:
Account posts identical comments across multiple unrelated posts within minutes.
Comments contain links to suspicious websites.
Account created 3 days ago but has 200+ comments.
Poor report example:
This is a bot.

Enabling Comment Collection

  1. Understand what’s collected - Review privacy section above
  2. Configure authentication - Required for upload
  3. Enable in settings - Toggle “Collecting Comments”
  4. Browse normally - Collection happens automatically
  5. Monitor storage - Check extension to see collection stats

Build docs developers (and LLMs) love