Inside Guming’s Frontend SDK: Design, Logging, and Tracking Explained

This article walks through the architecture and detailed design of Guming’s frontend data‑center SDK, covering its initialization, payload structure, core modules such as Configurator, Reporter, Queue, and various plugins, and explains how logging, tracking, rate‑limiting, and remote configuration are implemented for reliable data collection.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
Inside Guming’s Frontend SDK: Design, Logging, and Tracking Explained

Preface

In the previous article we discussed the overall architecture of Guming’s frontend data center; this article dives into the SDK side design.

Recall the previous architecture diagram if needed.

Overall Design

Overview Diagram

Usage

don't talk, show you the code
// 初始化
Track.init({
  debug: false,
  appId: 1,
  initialExtra: () => {
    return {
      // 强行覆盖默认的 appId,用于微应用中识别子应用
      appId: getCurrentApp()?.appId || 1,
      userId,
    };
  },
  integrations: {
    InstrumentTrack: {
      enable: false,
      option: {},
    },
  },
});

// 埋点,两种埋点方式,对于无需参数的可以快捷埋点
function submitTrack(eventName: string, options?: { extends?: any }): void;
function submitTrack({
  et: string;
  e_name: string;
  extends?: any;
}): void;

// 日志,消息内容会被 stringify
interface logger {
  error(msg: any): void;
  warn(msg: any): void;
  info(msg: any): void;
}

In daily use we normally do not use the core package directly; a platform package is provided that wraps the core for convenient logging, plugin integration, and custom APIs.

The platform exposes three functions: init, submitTrack, and logger, which handle initialization, tracking, and log reporting respectively.

Interface Format Design

The SDK only calls a single report interface, which carries all log information and cloud‑side configuration.

{
  "m": [
    {
      "time": 1710123186431,
      "referer": "http://localhost:3000/",
      "type": "log",
      "data": {}
    }
  ],
  "c": {
    "appId": 382,
    "env": "prod",
    "app": "",
    "app_version": "",
    "platform": "",
    "platform_version": "",
    "model": "",
    "brand": "",
    "userId": "123",
    "track": {}
  }
}

The payload contains the m field for log entries and the c field for common information such as appId, environment, and userId.

Why is there a time field? The client‑side time is trusted to compensate for delayed reporting caused by queueing, throttling, or network latency.

Modules

CrossPlatform : provides cross‑platform utilities such as storage and routing queue.

Reporter & Queue : controls log reporting, manages queues, throttling, and concurrency.

ExtraInfo : merges built‑in, plugin, and user extra information.

Configurator : fetches remote configuration.

Breadcrumb : records user‑behavior logs.

Event : global event communication.

Integrations : plugin system for registration and usage.

Plugins

api : API specification and exception monitoring.

behavior : user‑behavior log monitoring.

cache : log cache for loss‑prevention during throttling.

static : static‑resource monitoring.

track : tracking module.

Detailed Design

Init

The exposed init method wraps the SDK class instantiation.

class SDK {
  static get instance(): SDK;
  static init(option) {
    try {
      _global.__sdk_instance_ = new SDK(option);
    } catch (error) {
      console.error('sdk init error: ', error);
    }
  }
  readonly configurator: Configurator;
  readonly reporter: Reporter;
  // other built‑in modules
  private integrations: Record<string, Integration> = {};
  constructor() {
    this.configurator = new Configurator(this);
    this.extraInfo = new ExtraInfo(this);
    // initialize modules & load plugins
    const totalSwitch = this.initIntegration();
    const { report } = configurator.configuration;
    if (report === false || (typeof report === 'number' && report !== totalSwitch)) {
      configurator.forceUpdate();
    }
  }
}
export function logger(): Logger;
export function submitTrack(): SubmitTrack;
const DEFAULT_INTEGRATIONS = [
  InstrumentBehavior,
  InstrumentApi,
  InstrumentStatic,
  InstrumentTrack,
  IntegrationCacheData,
];
export init = (options) => {
  SDK.init({
    ...options,
    transport,
    corssPlatform,
    integrations(this) {
      return convertOptionsToIntegrations.call(this, DEFAULT_INTEGRATIONS, options.integrations);
    },
    initialExtra: () => inheritData(env.getTags.bind(env), initialExtra)
  });
};

The core SDK class is not developer‑facing; it focuses on initializing all modules in the correct order and loading plugins before fetching remote configuration.

The platform package already bundles default plugins and platform‑specific APIs, so developers only need to configure their own application settings.

Configurator

The Configurator fetches and processes remote configuration such as queue length, throttling switches, and rate‑limit parameters to protect the system during traffic spikes.

class Configurator {
  private configuration: Configuration = {};
  private stringConfiguration = '';
  private storage: Required<CrossPlatform>['storage'];
  /** force refresh configuration */
  forceUpdate(): void;
  /** parse configuration string */
  parse(input?: string, disableCache = false): Configuration;
  /** get specific configuration */
  get(key: string | string[]): any | Record<string, any>;
}

After a report, the server may return headers like a=2;b=25;c=31;, which are parsed into JSON for later use. The SDK also calls forceUpdate after initialization to ensure the latest config is applied.

In web, sendBeacon is often used for log reporting, but it does not provide response headers and has content‑type restrictions in certain Chrome versions.

Reporter & Queue

Reporter and Queue are the core of the SDK, deciding when and how logs are sent.

class Reporter {
  private queue: Queue;
  getQueue(): Queue;
  send(data: Data, options: { lazy?: boolean; reportType?: number });
}
class Queue {
  private stack = [];
  private immediatelyStack = [];
  private samplingList = [];
  private isFlushing = false;
  private timer: NodeJS.Timer | null = null;
  private retryTimes = 0;
  add(rawData: data, options: { lazy?: boolean }): void;
  report(data: Item[], options?: { lazy?: boolean }): void;
  getStack(): Item[];
  // other internal methods omitted for brevity
}

Queue triggers reporting when its length reaches a threshold, on a periodic timer (default 10 s), or when server‑side throttling is enabled. It also supports sampling and sharding logic.

Length‑based reporting: default threshold 25, configurable remotely.

Timed reporting: clears the queue every 10 seconds to avoid large time gaps.

Throttling: when enabled, uses userId to spread reports over time.

Sampling reduces the amount of data sent when the cloud enables it; sharding (抽样) drops entire user data based on a feature flag while preserving data for selected users.

Integrations

Integrations load external plugins during SDK initialization.

(_integrations ?? []).forEach((Integration) => {
  try {
    const integrationInstance =
      typeof Integration === 'object' ? Integration : new Integration(this);
    integrationInstance.instrument();
    this.integrations[Integration.constructor.name] = integrationInstance;
    totalSwitch += integrationInstance.reportType || 0;
  } catch (error) {
    console.error(error);
    this.logger.error(error?.message, 'sdk_error');
  }
});

Cache Plugin

The cache plugin stores logs locally when throttling is active and re‑sends them after the application restarts.

class Cache {
  /** initialize plugin, db etc. */
  instrument(): void;
  /** listen to unload event and cache the stack */
  addUnloadEvent(db: DB): void;
  /** re‑insert previous list on init */
  reInsertPrevList(db: DB): void;
}
class DB {
  static isSupport: boolean = !!(_global && _global.indexedDB);
  private db?: DBDatabase;
  connect(options: { name: string; version?: number; tableSchema: Record<string, string | undefined> }): Promise<void>;
  getAll<E = any>(tableName: string): Promise<E[]>;
  batchAdd(tableName: string, value: any, key?: string): Promise<void>;
  clear(table: string): Promise<void>;
}

Static Plugin

The static plugin hijacks onload and onerror events of resources to report them. It uses PerformanceObserver for web resources and a Babel plugin for mini‑program image tags.

const observer = new PerformanceObserver((list) => {
  // handle resources
});
try {
  observer.observe({ type: 'resource', buffer: true });
} catch {
  observer.observe({ entryTypes: ['resource'] });
}

Track Plugin

The track plugin re‑uses the platform’s _track implementation and adds automatic exposure and click tracking for scroll views and swipers.

class Track {
  instrument() {
    initTrack({
      ...options!,
      eventQueueLimitNum: 1,
      request: ({ data }) => {
        this.report({ type: MeasurementType.Track, data });
      },
    });
    sdk.extraInfo.addExtra('track', () => trackStore.getCommonInfo());
    this.trackClick();
  }
  /** hijack clicks with data‑track‑option attribute */
  private trackClick(): void;
}

Conclusion

The client‑side SDK must balance data reporting, performance, stability, and extensibility. Its design ensures reliability under increasing traffic and provides solid data support for products.

frontendSDKConfigurationtracking
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.