@tinkoff/perfume 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Leonardo Zizzamia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,524 @@
1
- # Temporary Holding Version
1
+ <a href="https://zizzamia.github.io/perfume/">
2
+ <img src="https://github.com/Zizzamia/perfume.js/blob/master/docs/src/assets/perfume-logo-v5-0-0.png"
3
+ align="left" width="200" alt="Perfume.js logo" />
4
+ </a>
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # [Perfume.js v9.4.0](https://zizzamia.github.io/perfume/)
7
+
8
+ [![Current version](https://img.shields.io/github/tag/zizzamia/perfume.js?color=3498DB&label=version)](https://www.npmjs.org/package/perfume.js) [![Test Coverage](https://api.codeclimate.com/v1/badges/f813d2f45b274d93b8c5/test_coverage)](https://codeclimate.com/github/Zizzamia/perfume.js/test_coverage) <img alt="No dependencies" src="https://img.shields.io/badge/dependencies-none-27ae60.svg"> [![Build Status](https://travis-ci.org/Zizzamia/perfume.js.svg?branch=master)](https://travis-ci.org/Zizzamia/perfume.js) [![NPM Downloads](http://img.shields.io/npm/dm/perfume.js.svg)](https://www.npmjs.org/package/perfume.js) [![gzip size](https://img.badgesize.io/https://unpkg.com/perfume.js?compression=gzip&label=JS+gzip+size)](https://unpkg.com/perfume.js) [![brotli size](https://img.badgesize.io/https://unpkg.com/perfume.js?compression=brotli&label=JS+brotli+size)](https://unpkg.com/perfume.js)
9
+
10
+ > <b>Page Speed</b> is a feature, and to deliver it we need to understand the many factors and fundamental limitations that are at play. If we can measure it, we can improve it.
11
+
12
+ <br />
13
+ <br />
14
+ <br />
15
+ <br />
16
+
17
+ ## Why Perfume.js?
18
+
19
+ Perfume is a tiny, web performance monitoring library that reports field data back to your favorite analytics tool.
20
+
21
+ - ⏰ Supports latest Performance APIs for precise metrics
22
+ - 🚀 Device data enrichment
23
+ - 🔨 Cross browser tested
24
+ - 🚿 Filters out false positive/negative results
25
+ - 🤙 Only 5.1Kb gzip
26
+ - 🏅 Web Vitals Score
27
+ - 🛰 Flexible analytics tool
28
+ - ⚡️ Waste-zero ms with [requestIdleCallback](https://developers.google.com/web/updates/2015/08/using-requestidlecallback) strategy built-in
29
+ - Ability to track data about user actions
30
+ <br />
31
+
32
+ ## The latest in metrics & Real User Measurement
33
+
34
+ **Perfume** leverages the latest Performance APIs to collect **field data** that allows us to understand what real-world users are actually experiencing.
35
+
36
+ - [Navigation Timing](#navigation-timing)
37
+ - Navigator Interface
38
+ - [Resource Timing](#resource-timing)
39
+ - [Element Timing](#element-timing)
40
+ - Service Worker Status
41
+ - StorageManager interface
42
+ - Time to First Byte ([TTFB](https://web.dev/ttfb/#what-is-a-good-ttfb-score))
43
+ - First Contentful Paint ([FCP](https://web.dev/first-contentful-paint/))
44
+ - Largest Contentful Paint ([LCP](https://web.dev/lcp/))
45
+ - First Input Delay ([FID](https://web.dev/fid/))
46
+ - Cumulative Layout Shift ([CLS](https://web.dev/cls/))
47
+ - Interaction to Next Paint ([INP](https://web.dev/inp/))
48
+ - Total Blocking Time ([TBT](https://web.dev/tbt/))
49
+ - Navigation Total Blocking Time ([NTBT](#navigation-total-blocking-time-ntbt))
50
+
51
+ <br />
52
+ At <a href="https://www.coinbase.com/blog/performance-vitals-a-unified-scoring-system-to-guide-performance-health-and-prioritization">Coinbase</a>, we use Perfume.js to capture a high-level scoring system that is clear, trusted, and easy to understand.
53
+ <br />
54
+ <br />
55
+ Summarizing the performance health of an application into a reliable and consistent score helps increase urgency and directs company attention and resources towards addressing each performance opportunity.
56
+ <br />
57
+
58
+ ## Perfume.js vs [Web Vitals](https://github.com/GoogleChrome/web-vitals)
59
+
60
+ **Perfume** leverages the [Web Vitals](https://github.com/GoogleChrome/web-vitals) library to collect all the standardized performance metrics. It explores new metrics like Navigation Total Blocking Time and dimensions like Low-End Devices, Service Worker status to understand your data better.
61
+
62
+ So don't worry, Perfume.js is a superset of Web Vitals, a bit like Typescript is a superset of Javascript.
63
+
64
+ ## Installing
65
+
66
+ npm (https://www.npmjs.com/package/perfume.js):
67
+
68
+ npm install perfume.js --save
69
+
70
+ ### Importing library
71
+
72
+ You can import the generated bundle to use the whole library generated:
73
+
74
+ ```javascript
75
+ import { initPerfume } from 'perfume.js';
76
+ ```
77
+
78
+ Universal Module Definition:
79
+
80
+ ```javascript
81
+ import { initPerfume } from 'node_modules/perfume.js/dist/perfume.umd.min.js';
82
+ ```
83
+
84
+ <br />
85
+
86
+ ## Quick start
87
+
88
+ Metrics like **Navigation Timing**, **Network Information**, **TTFB**, **FCP**, **FID**, **LCP**, **CLS**, **INP** and **TBT** are default reported with Perfume; All results will be reported to the `analyticsTracker` callback, and the code below is just one way for you to organize your tracking, feel free to tweak it suit your needs.
89
+
90
+ 🚀 Visit [https://zizzamia.github.io/perfume/](https://zizzamia.github.io/perfume/) for a live demo on how the metrics work. 🌕
91
+
92
+ ```javascript
93
+ import { initPerfume } from 'perfume.js';
94
+
95
+ initPerfume({
96
+ analyticsTracker: options => {
97
+ const {
98
+ attribution,
99
+ metricName,
100
+ data,
101
+ navigatorInformation,
102
+ rating,
103
+ navigationType,
104
+ } = options;
105
+ switch (metricName) {
106
+ case 'navigationTiming':
107
+ if (data && data.timeToFirstByte) {
108
+ myAnalyticsTool.track('navigationTiming', data);
109
+ }
110
+ break;
111
+ case 'networkInformation':
112
+ if (data && data.effectiveType) {
113
+ myAnalyticsTool.track('networkInformation', data);
114
+ }
115
+ break;
116
+ case 'storageEstimate':
117
+ myAnalyticsTool.track('storageEstimate', data);
118
+ break;
119
+ case 'TTFB':
120
+ myAnalyticsTool.track('timeToFirstByte', { duration: data });
121
+ break;
122
+ case 'RT':
123
+ myAnalyticsTool.track('redirectTime', { duration: data });
124
+ break;
125
+ case 'FCP':
126
+ myAnalyticsTool.track('firstContentfulPaint', { duration: data });
127
+ break;
128
+ case 'FID':
129
+ myAnalyticsTool.track('firstInputDelay', { duration: data });
130
+ break;
131
+ case 'LCP':
132
+ myAnalyticsTool.track('largestContentfulPaint', { duration: data });
133
+ break;
134
+ case 'CLS':
135
+ myAnalyticsTool.track('cumulativeLayoutShift', { value: data });
136
+ break;
137
+ case 'INP':
138
+ myAnalyticsTool.track('interactionToNextPaint', { value: data });
139
+ break;
140
+ case 'TBT':
141
+ myAnalyticsTool.track('totalBlockingTime', { duration: data });
142
+ break;
143
+ case 'elPageTitle':
144
+ myAnalyticsTool.track('elementTimingPageTitle', { duration: data });
145
+ break;
146
+ case 'userJourneyStep':
147
+ myAnalyticsTool.track('userJourneyStep', {
148
+ duration: data,
149
+ stepName: attribution.step_name,
150
+ vitals_score: rating,
151
+ });
152
+ break;
153
+ default:
154
+ myAnalyticsTool.track(metricName, { duration: data });
155
+ break;
156
+ }
157
+ },
158
+ });
159
+ ```
160
+
161
+ In a world with widely varying device capabilities, a one-size-fits-all event doesn’t always work. Perfume adds **data enrichment** to all events so we can better understand the real world experiences:
162
+
163
+ - **deviceMemory**: the user's device memory (RAM).
164
+ - **hardwareConcurrency**: the number of logical CPU processor cores on the user's device.
165
+ - **serviceWorkerStatus**: status of the service worker between controlled, supported and unsupported.
166
+
167
+ Based on the Navigator APIs the library can help us differentiate between a low-end and a high-end device/experience:
168
+
169
+ - **isLowEndDevice**: combination of the score of RAM and CPU.
170
+ - **isLowEndExperience**: combination of the score of RAM, CPU, NetworkStatus and SaveData.
171
+
172
+ ## Performance audits
173
+
174
+ Coo coo coo [cool](https://www.youtube.com/watch?v=zDcbpFimUc8), let's learn something new.
175
+
176
+ ### Navigation Timing
177
+
178
+ Navigation Timing collects performance metrics for the life and timings of a network request.
179
+
180
+ Perfume helps expose some of the key metrics you might need.
181
+
182
+ <ul>
183
+ <li><b>Redirect time</b>: Page redirects aren't totally inconsequential, but they might not be something you run into very often. Still, redirects add latency to requests, so measuring them may be worth the effort.</li>
184
+ <li><b>DNS lookup</b>: When a user requests a URL, the Domain Name System (DNS) is queried to translate a domain to an IP address.</li>
185
+ <li><b>Header size</b>: HTTP header size</li>
186
+ <li><b>Fetch time</b>: Cache seek plus response time</li>
187
+ <li><b>Worker time</b>: Service worker time plus response time</li>
188
+ <li><b>Total time</b>: Request plus response time (network only)</li>
189
+ <li><b>Download time</b>: Response time only (download)</li>
190
+ <li><b>Time to First Byte</b>: The amount of time it takes after the client sends an HTTP GET request to receive the first byte of the requested resource from the server.
191
+ It is the largest web page load time component taking 40 to 60% of total web page latency.</li>
192
+ </ul>
193
+
194
+ ```javascript
195
+ // Perfume.js: navigationTiming { ... timeToFirstByte: 192.65 }
196
+ ```
197
+
198
+ ### First Paint ([FP](https://medium.com/@zizzamia/first-contentful-paint-with-a-touch-of-perfume-js-cd11dfd2e18f))
199
+
200
+ **FP** is the exact time taken for the browser to render anything as visually different from what was on the screen before navigation, e.g. a background change after a long blank white screen time.
201
+
202
+ ```javascript
203
+ // Perfume.js: fp 1482.00 ms
204
+ ```
205
+
206
+ ### Navigation Total Blocking Time (NTBT)
207
+
208
+ This metric measures the amount of time the application may be blocked from processing code during the 2s window after a user navigates from page A to page B. The NTBT metric is the summation of the blocking time of all long tasks in the 2s window after this method is invoked.
209
+
210
+ Because this library is navigation agnostic, we have this method to mark when the navigation starts.
211
+
212
+ If this method is called before the 2s window ends; it will trigger a new NTBT measurement and interrupt the previous one.
213
+
214
+ ```javascript
215
+ import { createBrowserHistory } from 'history';
216
+ export const history = createBrowserHistory();
217
+
218
+ initPerfume({
219
+ analyticsTracker: ({ metricName, data }) => {
220
+ myAnalyticsTool.track(metricName, data);
221
+ })
222
+ });
223
+
224
+ // React custom history
225
+ history.listen(() => {
226
+ // Measure NTBT at the beginning of each navigation
227
+ perfume.markNTBT();
228
+ });
229
+
230
+ // Perfume.js: ntbt 78 ms
231
+ ```
232
+
233
+ ### Resource Timing
234
+
235
+ Resource Timing collects performance metrics for document-dependent resources. Stuff like style sheets, scripts, images, et cetera.
236
+ Perfume helps expose all PerformanceResourceTiming entries and groups data data consumption by Kb used.
237
+
238
+ ```javascript
239
+ initPerfume({
240
+ resourceTiming: true,
241
+ analyticsTracker: ({ metricName, data }) => {
242
+ myAnalyticsTool.track(metricName, data);
243
+ })
244
+ });
245
+ // Perfume.js: dataConsumption { "css": 185.95, "fetch": 0, "img": 377.93, ... , "script": 8344.95 }
246
+ ```
247
+
248
+ ### Annotate metrics in the DevTools
249
+
250
+ **Performance.mark** ([User Timing API](https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API)) is used to create an application-defined peformance entry in the browser's performance entry buffer.
251
+
252
+ ```javascript
253
+ import { start, end } from 'perfume.js';
254
+
255
+ initPerfume({
256
+ analyticsTracker: ({ metricName, data }) => {
257
+ myAnalyticsTool.track(metricName, data);
258
+ })
259
+ });
260
+ start('fibonacci');
261
+ fibonacci(400);
262
+ end('fibonacci');
263
+ // Perfume.js: fibonacci 0.14 ms
264
+ ```
265
+
266
+ ![Performance Mark](https://github.com/Zizzamia/perfume.js/blob/master/docs/src/assets/performance-mark.png)
267
+
268
+ ### Element Timing
269
+
270
+ Track when image elements and text nodes are displayed on screen using the [emerging](https://chromestatus.com/features#elementtiming) [Element Timing API](https://wicg.github.io/element-timing/) specification by simply adding the `elementtiming` attribute with a descriptive value of your choice to HTML elements you would like to measure:
271
+
272
+ ```html
273
+ <h1 elementtiming="elPageTitle" class="title">Perfume.js</h1>
274
+ <img
275
+ elementtiming="elHeroLogo"
276
+ alt="Perfume.js logo"
277
+ src="https://zizzamia.github.io/perfume/assets/perfume-logo-v5-0-0.png"
278
+ />
279
+ ```
280
+
281
+ ```javascript
282
+ initPerfume({
283
+ elementTiming: true,
284
+ analyticsTracker: ({ metricName, data }) => {
285
+ myAnalyticsTool.track(metricName, data);
286
+ })
287
+ });
288
+
289
+ // Perfume.js: elPageTitle 256.00 ms
290
+ // Perfume.js: elHeroLogo 1234.00 ms
291
+ ```
292
+
293
+ ### User Journey Step Tracking
294
+
295
+ A Step represents a slice of time in the User Journey where the user is blocked by **system time**. System time is time the system is blocking the user. For example, the time it takes to navigate between screens or fetch critical information from the server. This should not be confused with **cognitive time**, which is the time the user spends thinking about what to do next. User Journey steps should only cover system time.
296
+
297
+ A Step is defined by an event to start the step, and another event to end the step. These events are referred to as **Marks**.
298
+
299
+ As an example, a Step could be to navigate from screen A to screen B. The appropriate way to mark a start and end to this step is by marking the start when tapping on the button on screen A that starts the navigation and marking the end when screen B comes into focus with the critical data rendered on the screen.
300
+
301
+ ```typescript
302
+ // Marking the start of the step
303
+ const ScreenA = () => {
304
+ const handleNavigation = () => {
305
+ ... // Navigation logic
306
+ // Mark when navigating to screen B
307
+ markStep('navigate_to_screen_B');
308
+ }
309
+
310
+ ...
311
+
312
+ return (
313
+ <>
314
+ <Button onPress={handleNavigation} />
315
+ </>
316
+ );
317
+ }
318
+
319
+ // Marking the end of the step
320
+ const ScreenB = () => {
321
+ const { viewer } = fetch("http://example.com/userInfo")
322
+ .then((response) => response.json())
323
+ .then((data) => data);
324
+
325
+ const {name} = viewer.userProperties;
326
+
327
+ useEffect(() => {
328
+ if (name) {
329
+ // Mark when data is ready for screen B
330
+ markStep('loaded_screen_B');
331
+ }
332
+ }, [name])
333
+
334
+ ...
335
+ }
336
+ ```
337
+
338
+ #### Defining Steps
339
+
340
+ In order for Perfume to be able to track metrics for Steps, we need to configure the steps and provide them when initializing Perfume.
341
+
342
+ Below you can find an example of how to do this.
343
+
344
+ ```typescript
345
+ export const steps = {
346
+ load_screen_A: {
347
+ threshold: ThresholdTier.quick,
348
+ marks: ['navigate_to_screen_A', 'loaded_screen_A'],
349
+ },
350
+ load_screen_B: {
351
+ threshold: ThresholdTier.quick,
352
+ marks: ['navigate_to_screen_B', 'loaded_screen_B'],
353
+ },
354
+ };
355
+
356
+ initPerfume({ steps });
357
+ ```
358
+
359
+ #### MarkStep
360
+
361
+ `markStep` is the function used to start and end steps in applications.
362
+
363
+ For example, if we wanted to mark the beginning of load_screen_B step above, we would add in `markStep('navigate_to_screen_B')` to our code.
364
+
365
+ #### `trackUJNavigation`
366
+
367
+ The purpose of this function is to only account for active steps that the user is working on. The feature will remove any inactive or 'stale' steps that are not currently in progress.
368
+
369
+ Stale steps can be created by navigating away from a page before it fully loads, this would cause the start mark to be triggered, but the end mark to not be called. This would affect the `active` steps being returned to `onMarkStep` as well as would create incorrect data if we returned back to the end mark much later than expected.
370
+
371
+ The `trackUJNavigation` function is to be called anytime there is a navigation change in your application. Below is an example for how this would work in a React Application:
372
+
373
+ ```typescript
374
+ import { useLocation } from 'react-router-dom';
375
+
376
+ const MyComponent = () => {
377
+ const location = useLocation()
378
+
379
+ React.useEffect(() => {
380
+ // runs on location, i.e. route, change
381
+ trackUJNavigation();
382
+ }, [location])
383
+ ...
384
+ }
385
+
386
+ ```
387
+
388
+ ## Web Vitals Score
389
+
390
+ Perfume will expose for all major metrics the vitals score, those can be used to improve your [SEO and Google page rank](https://webmasters.googleblog.com/2020/05/evaluating-page-experience.html).
391
+
392
+ | Web Vitals | Good | Needs Improvement | Poor |
393
+ | ------------------------------------- | -----: | ----------------: | --------: |
394
+ | Time to First Byte (TTFB) | 0-800 | 801-1800 | Over 1800 |
395
+ | Redirect Time (RT) | 0-100 | 101-200 | Over 200 |
396
+ | First Contentful Paint (FCP) | 0-2000 | 2001-4000 | Over 4000 |
397
+ | Largest Contentful Paint (LCP) | 0-2500 | 2501-4000 | Over 4000 |
398
+ | First Input Delay (FID) | 0-100 | 101-300 | Over 300 |
399
+ | Cumulative Layout Shift (CLS) | 0-0.1 | 0.11-0.25 | Over 0.25 |
400
+ | Interaction to Next Paint (INP) | 0-200 | 201-500 | Over 500 |
401
+ | Total Blocking Time (TBT) | 0-200 | 201-600 | Over 600 |
402
+ | Navigation Total Blocking Time (NTBT) | 0-200 | 201-600 | Over 600 |
403
+
404
+ Step Tracking is based on various [thresholds](https://github.com/Zizzamia/perfume.js/blob/master/__tests__/stepsTestConstants.ts) defined.
405
+
406
+ Below are the thresholds available for each step:
407
+
408
+ | Label | Vital Thresholds |
409
+ | ----------- | ---------------- |
410
+ | INSTANT | [100, 200] |
411
+ | QUICK | [200, 500] |
412
+ | MODERATE | [500, 1000] |
413
+ | SLOW | [1000, 2000] |
414
+ | UNAVOIDABLE | [2000, 5000] |
415
+
416
+ ## Perfume custom options
417
+
418
+ Default options provided to Perfume.js constructor.
419
+
420
+ ```javascript
421
+ const options = {
422
+ resourceTiming: false,
423
+ elementTiming: false,
424
+ analyticsTracker: options => {},
425
+ maxMeasureTime: 30000,
426
+ enableNavigtionTracking: true,
427
+ };
428
+ ```
429
+
430
+ ## Use Google Analytics
431
+
432
+ A quick way to see your page speed results on your web app is by using Google Analytics. Those GA events will show on Behavior > Site Speed > User Timings. For testing you might want to see them coming live on Realtime > Events.
433
+
434
+ Have fun ✨
435
+
436
+ ```javascript
437
+ const metricNames = ['TTFB', 'RT', 'FCP', 'LCP', 'FID', 'CLS', 'TBT'];
438
+ initPerfume({
439
+ analyticsTracker: ({ attribution, metricName, data, navigatorInformation, rating, navigationType }) => {
440
+ if (metricNames.includes(metricName)) {
441
+ ga('send', 'event', {
442
+ eventCategory: 'Perfume.js',
443
+ eventAction: metricName,
444
+ // Google Analytics metrics must be integers, so the value is rounded
445
+ eventValue: metricName === 'cls' ? data * 1000 : data,
446
+ eventLabel: navigatorInformation.isLowEndExperience ? 'lowEndExperience' : 'highEndExperience',
447
+ // Use a non-interaction event to avoid affecting bounce rate
448
+ nonInteraction: true,
449
+ });
450
+ }
451
+ })
452
+ });
453
+ ```
454
+
455
+ To connect with additional analytics providers, checkout the [analytics plugin for Perfume.js](https://getanalytics.io/plugins/perfumejs/).
456
+
457
+ <br />
458
+
459
+ ## Develop
460
+
461
+ - `npm run test`: Run test suite
462
+ - `npm run build`: Generate bundles and typings
463
+ - `npm run lint`: Lints code
464
+ <br />
465
+
466
+ ## Plugins
467
+
468
+ - [Perfume.js plugin for Analytics](https://github.com/DavidWells/analytics/tree/master/packages/analytics-plugin-perfumejs)
469
+ <br />
470
+
471
+ ## Perfume is used by
472
+
473
+ - [Conio](https://business.conio.com/)
474
+ - [Coinbase](https://www.coinbase.com)
475
+ - [Coinbase Pro](https://pro.coinbase.com)
476
+ - [Coinbase Custody](https://custody.coinbase.com)
477
+ - [Financial-Times](https://github.com/Financial-Times/n-tracking)
478
+ - [Hearst](https://www.cosmopolitan.com/)
479
+ - [Plan](https://getplan.co)
480
+ - Add your company name :)
481
+ <br />
482
+
483
+ ## Credits and Specs
484
+
485
+ Made with ☕️ by [@zizzamia](https://twitter.com/zizzamia) and
486
+ I want to thank some friends and projects for the work they did:
487
+
488
+ - [Leraging the Performance Metrics that Most Affect User Experience](https://developers.google.com/web/updates/2017/06/user-centric-performance-metrics) for documenting this new User-centric performance metrics;ev
489
+ - [Performance Timeline Level 2](https://w3c.github.io/performance-timeline/) the definition of _PerformanceObserver_ in that specification;
490
+ - [The Contributors](https://github.com/Zizzamia/perfume.js/graphs/contributors) for their much appreciated Pull Requests and bug reports;
491
+ - **you** for the star you'll give this project 😉 and for supporting me by giving my project a try 😄
492
+
493
+ ### Contributors
494
+
495
+ This project exists thanks to all the people who contribute.
496
+ <img src="https://opencollective.com/perfumejs/contributors.svg?width=890&button=false" />
497
+
498
+ ### Backers
499
+
500
+ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/perfumejs#backer)]
501
+
502
+ <a href="https://opencollective.com/perfumejs#backers" target="_blank"><img src="https://opencollective.com/perfumejs/backers.svg?width=890"></a>
503
+
504
+ <br />
505
+
506
+ ## Copyright and license
507
+
508
+ Code and documentation copyright 2022 [Leonardo Zizzamia](https://twitter.com/Zizzamia). Code released under the [MIT license](LICENSE). Docs released under Creative Commons.
509
+
510
+ ## Team
511
+
512
+ <table>
513
+ <tbody>
514
+ <tr>
515
+ <td align="center" valign="top">
516
+ <a href="https://twitter.com/Zizzamia">
517
+ <img width="100" height="100" src="https://github.com/zizzamia.png?s=150">
518
+ </a>
519
+ <br />
520
+ <a href="https://twitter.com/Zizzamia">Leonardo Zizzamia</a>
521
+ </td>
522
+ </tr>
523
+ </tbody>
524
+ </table>