• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dev-ignis / cross-log / 17016589983

17 Aug 2025 04:13AM UTC coverage: 87.462% (+1.8%) from 85.706%
17016589983

push

github

dev-ignis
test: - threshold;

673 of 840 branches covered (80.12%)

Branch coverage included in aggregate %.

1071 of 1154 relevant lines covered (92.81%)

393.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.2
/src/plugins/performance.ts
1
import { PluginContext, PerformancePlugin, PerformancePluginConfig, WebVitalsMetrics, PluginInstance } from './types';
2
import { TypedPlugin } from './plugin-types';
3

4
export class PerformanceMetricsPlugin implements TypedPlugin<'performance'> {
30✔
5
  name: 'performance' = 'performance';
87✔
6
  version = '1.0.0';
87✔
7
  config: PerformancePluginConfig;
8
  methods?: PerformancePlugin;
9
  private context?: PluginContext;
10
  private marks: Map<string, number> = new Map();
87✔
11

12
  constructor(config?: PerformancePluginConfig) {
13
    this.config = {
87✔
14
      enabled: true,
15
      webVitals: true,
16
      resourceTiming: true,
17
      thresholds: {
18
        fcp: 1800,
19
        lcp: 2500,
20
        fid: 100,
21
        cls: 0.1,
22
        ttfb: 800
23
      },
24
      ...config
25
    };
26
  }
27

28
  init(context: PluginContext): void {
29
    this.context = context;
87✔
30

31
    const methods: PerformancePlugin = {
87✔
32
      measure: this.measure.bind(this),
33
      mark: this.mark.bind(this),
34
      webVitals: this.webVitals.bind(this),
35
      resource: this.resource.bind(this),
36
      cache: this.cache.bind(this)
37
    };
38

39
    (context as any).methods = methods;
87✔
40
    
41
    const instance = context as unknown as PluginInstance;
87✔
42
    instance.methods = methods as any;
87✔
43

44
    if (this.config.webVitals && typeof window !== 'undefined') {
87✔
45
      this.observeWebVitals();
3✔
46
    }
47
  }
48

49
  private observeWebVitals(): void {
50
    if (typeof PerformanceObserver === 'undefined') return;
3!
51

52
    try {
3✔
53
      const observer = new PerformanceObserver((list) => {
3✔
54
        for (const entry of list.getEntries()) {
×
55
          if (entry.entryType === 'paint') {
×
56
            if (entry.name === 'first-contentful-paint') {
×
57
              this.webVitals({ fcp: entry.startTime });
×
58
            }
59
          } else if (entry.entryType === 'largest-contentful-paint') {
×
60
            this.webVitals({ lcp: entry.startTime });
×
61
          } else if (entry.entryType === 'first-input') {
×
62
            const fidEntry = entry as any;
×
63
            this.webVitals({ fid: fidEntry.processingStart - fidEntry.startTime });
×
64
          } else if (entry.entryType === 'layout-shift') {
×
65
            const clsEntry = entry as any;
×
66
            if (!clsEntry.hadRecentInput) {
×
67
              this.webVitals({ cls: clsEntry.value });
×
68
            }
69
          }
70
        }
71
      });
72

73
      observer.observe({ entryTypes: ['paint', 'largest-contentful-paint', 'first-input', 'layout-shift'] });
3✔
74
    } catch (error) {
75
      this.context?.logger.debug('Failed to observe Web Vitals', 'Performance', error);
×
76
    }
77
  }
78

79
  private formatDuration(duration: number): string {
80
    if (duration < 1000) return `${duration.toFixed(2)}ms`;
66✔
81
    return `${(duration / 1000).toFixed(2)}s`;
12✔
82
  }
83

84
  measure(name: string, duration: number, metadata?: Record<string, any>): void {
85
    if (!this.context || !this.config.enabled) return;
21!
86

87
    const logData: Record<string, any> = {
21✔
88
      type: 'performance_measure',
89
      name,
90
      duration: this.formatDuration(duration),
91
      durationMs: duration,
92
      timestamp: new Date().toISOString()
93
    };
94

95
    if (metadata) {
21!
96
      logData.metadata = metadata;
×
97
    }
98

99
    const message = `Performance Measure: ${name} - ${this.formatDuration(duration)}`;
21✔
100

101
    this.context.logger.info(message, 'Performance', logData);
21✔
102
  }
103

104
  mark(name: string, metadata?: Record<string, any>): void {
105
    if (!this.context || !this.config.enabled) return;
18!
106

107
    const now = performance?.now() ?? Date.now();
18!
108
    const previousMark = this.marks.get(name);
18✔
109
    this.marks.set(name, now);
18✔
110

111
    const logData: Record<string, any> = {
18✔
112
      type: 'performance_mark',
113
      name,
114
      timestamp: new Date().toISOString(),
115
      markTime: now
116
    };
117

118
    if (previousMark !== undefined) {
18✔
119
      const duration = now - previousMark;
3✔
120
      logData.durationSinceLast = this.formatDuration(duration);
3✔
121
      logData.durationMs = duration;
3✔
122
    }
123

124
    if (metadata) {
18!
125
      logData.metadata = metadata;
×
126
    }
127

128
    const message = `Performance Mark: ${name}${
18✔
129
      previousMark !== undefined ? ` (${this.formatDuration(now - previousMark)} since last)` : ''
18✔
130
    }`;
131

132
    this.context.logger.debug(message, 'Performance', logData);
18✔
133
  }
134

135
  webVitals(metrics: WebVitalsMetrics): void {
136
    if (!this.context || !this.config.enabled || !this.config.webVitals) return;
9!
137

138
    const logData: Record<string, any> = {
9✔
139
      type: 'web_vitals',
140
      metrics: {},
141
      timestamp: new Date().toISOString()
142
    };
143

144
    const warnings: string[] = [];
9✔
145

146
    if (metrics.fcp !== undefined) {
9✔
147
      logData.metrics.fcp = metrics.fcp;
9✔
148
      if (this.config.thresholds?.fcp && metrics.fcp > this.config.thresholds.fcp) {
9!
149
        warnings.push(`FCP ${metrics.fcp.toFixed(2)}ms exceeds threshold ${this.config.thresholds.fcp}ms`);
6✔
150
      }
151
    }
152

153
    if (metrics.lcp !== undefined) {
9✔
154
      logData.metrics.lcp = metrics.lcp;
9✔
155
      if (this.config.thresholds?.lcp && metrics.lcp > this.config.thresholds.lcp) {
9!
156
        warnings.push(`LCP ${metrics.lcp.toFixed(2)}ms exceeds threshold ${this.config.thresholds.lcp}ms`);
6✔
157
      }
158
    }
159

160
    if (metrics.fid !== undefined) {
9!
161
      logData.metrics.fid = metrics.fid;
×
162
      if (this.config.thresholds?.fid && metrics.fid > this.config.thresholds.fid) {
×
163
        warnings.push(`FID ${metrics.fid.toFixed(2)}ms exceeds threshold ${this.config.thresholds.fid}ms`);
×
164
      }
165
    }
166

167
    if (metrics.cls !== undefined) {
9✔
168
      logData.metrics.cls = metrics.cls;
6✔
169
      if (this.config.thresholds?.cls && metrics.cls > this.config.thresholds.cls) {
6!
170
        warnings.push(`CLS ${metrics.cls.toFixed(3)} exceeds threshold ${this.config.thresholds.cls}`);
3✔
171
      }
172
    }
173

174
    if (metrics.ttfb !== undefined) {
9!
175
      logData.metrics.ttfb = metrics.ttfb;
×
176
      if (this.config.thresholds?.ttfb && metrics.ttfb > this.config.thresholds.ttfb) {
×
177
        warnings.push(`TTFB ${metrics.ttfb.toFixed(2)}ms exceeds threshold ${this.config.thresholds.ttfb}ms`);
×
178
      }
179
    }
180

181
    if (metrics.inp !== undefined) {
9!
182
      logData.metrics.inp = metrics.inp;
×
183
    }
184

185
    const metricNames = Object.keys(metrics).map(k => k.toUpperCase()).join(', ');
24✔
186
    const message = `Web Vitals: ${metricNames}`;
9✔
187

188
    if (warnings.length > 0) {
9✔
189
      logData.warnings = warnings;
6✔
190
      this.context.logger.warn(message, 'Performance', logData);
6✔
191
    } else {
192
      this.context.logger.info(message, 'Performance', logData);
3✔
193
    }
194
  }
195

196
  resource(name: string, type: string, duration: number, size?: number): void {
197
    if (!this.context || !this.config.enabled || !this.config.resourceTiming) return;
6!
198

199
    const logData: Record<string, any> = {
6✔
200
      type: 'resource_timing',
201
      resourceName: name,
202
      resourceType: type,
203
      duration: this.formatDuration(duration),
204
      durationMs: duration,
205
      timestamp: new Date().toISOString()
206
    };
207

208
    if (size !== undefined) {
6✔
209
      logData.size = size;
6✔
210
      logData.sizeFormatted = this.formatBytes(size);
6✔
211
    }
212

213
    const message = `Resource Load: ${type} - ${name} (${this.formatDuration(duration)})${
6✔
214
      size !== undefined ? ` - ${this.formatBytes(size)}` : ''
6!
215
    }`;
216

217
    if (duration > 3000) {
6✔
218
      this.context.logger.warn(message, 'Performance', logData);
3✔
219
    } else {
220
      this.context.logger.debug(message, 'Performance', logData);
3✔
221
    }
222
  }
223

224
  cache(operation: 'hit' | 'miss' | 'set' | 'delete', key: string, duration?: number): void {
225
    if (!this.context || !this.config.enabled) return;
9!
226

227
    const logData: Record<string, any> = {
9✔
228
      type: 'cache_operation',
229
      operation,
230
      key,
231
      timestamp: new Date().toISOString()
232
    };
233

234
    if (duration !== undefined) {
9✔
235
      logData.duration = this.formatDuration(duration);
3✔
236
      logData.durationMs = duration;
3✔
237
    }
238

239
    const emoji = {
9✔
240
      hit: '✓',
241
      miss: '✗',
242
      set: '+',
243
      delete: '-'
244
    }[operation];
245

246
    const message = `Cache ${operation.toUpperCase()} ${emoji}: ${key}${
9✔
247
      duration !== undefined ? ` (${this.formatDuration(duration)})` : ''
9✔
248
    }`;
249

250
    const level = operation === 'hit' ? 'debug' : operation === 'miss' ? 'info' : 'debug';
9!
251
    
252
    if (level === 'info') {
9✔
253
      this.context.logger.info(message, 'Performance', logData);
3✔
254
    } else {
255
      this.context.logger.debug(message, 'Performance', logData);
6✔
256
    }
257
  }
258

259
  private formatBytes(bytes: number): string {
260
    if (bytes === 0) return '0 B';
12!
261
    const k = 1024;
12✔
262
    const sizes = ['B', 'KB', 'MB', 'GB'];
12✔
263
    const i = Math.floor(Math.log(bytes) / Math.log(k));
12✔
264
    return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
12✔
265
  }
266

267
  destroy(): void {
268
    this.context = undefined;
3✔
269
    this.marks.clear();
3✔
270
  }
271
}
272

273
export function createPerformancePlugin(config?: PerformancePluginConfig): PerformanceMetricsPlugin {
30✔
274
  return new PerformanceMetricsPlugin(config);
87✔
275
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc