├── .editorconfig ├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── package ├── .releaserc.json ├── CHANGELOG.md ├── esbuild-runner.config.js ├── package-lock.json ├── package.json ├── scripts.ts ├── src │ ├── OpenAPI-Ingest.yaml │ ├── cdn │ │ └── client-script.ts │ ├── index.ts │ └── ingest-api-types.ts └── tsconfig.json └── usage ├── react └── react-project │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── About.tsx │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── standalone ├── about.html └── index.html ├── svelte └── svelte-project │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── About.svelte │ ├── App.svelte │ ├── Home.svelte │ ├── app.css │ ├── assets │ │ └── svelte.svg │ ├── lib │ │ └── Counter.svelte │ ├── main.ts │ └── vite-env.d.ts │ ├── svelte.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── vue └── vue-project ├── .gitignore ├── README.md ├── env.d.ts ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── assets │ ├── base.css │ ├── logo.svg │ └── main.css ├── components │ ├── HelloWorld.vue │ ├── TheWelcome.vue │ ├── WelcomeItem.vue │ └── icons │ │ ├── IconCommunity.vue │ │ ├── IconDocumentation.vue │ │ ├── IconEcosystem.vue │ │ ├── IconSupport.vue │ │ └── IconTooling.vue ├── main.ts ├── router │ └── index.ts └── views │ ├── AboutView.vue │ └── HomeView.vue ├── tsconfig.config.json ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 'Release' 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'package/**' 8 | - '.github/**' 9 | 10 | defaults: 11 | run: 12 | working-directory: ./package 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | # Otherwise the token this creates(?) overrides the workflow generated one, required for semantic-release to work 21 | # https://github.com/semantic-release/semantic-release/blob/master/docs/recipes/ci-configurations/github-actions.md#pushing-packagejson-changes-to-a-master-branch 22 | persist-credentials: false 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 18 26 | - run: npm install 27 | - run: npm run package 28 | - uses: actions/cache@v3 29 | with: 30 | path: ./ 31 | key: ${{ github.sha }} 32 | 33 | release: 34 | needs: build 35 | permissions: 36 | contents: write # Required to make a commit with the contents of the build output, the /dist folder 37 | issues: write # Required to close any issues that was referenced in the PR 38 | pull-requests: write # Read PRs and comment on them 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/cache@v3 42 | with: 43 | path: ./ 44 | key: ${{ github.sha }} 45 | - run: npx semantic-release 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | .wireit 4 | node_modules 5 | dist 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Website Analytics Client 2 | 3 | - [Serverless Website Analytics Client](#serverless-website-analytics-client) 4 | * [Usage](#usage) 5 | + [Standalone Import Script Usage](#standalone-import-script-usage) 6 | - [Tracking](#tracking) 7 | * [Attribute tracking](#attribute-tracking) 8 | * [Manual tracking](#manual-tracking) 9 | + [SDK Client Usage](#sdk-client-usage) 10 | - [Vue](#vue) 11 | - [React](#react) 12 | - [Svelte](#svelte) 13 | * [Package src](#package-src) 14 | * [Developing](#developing) 15 | * [Contributing](#contributing) 16 | * [FAQ](#faq) 17 | + [The network calls to the backend fail with 403](#the-network-calls-to-the-backend-fail-with-403) 18 | + [Why not fetch with `keepalive` in the client](#why-not-fetch-with--keepalive--in-the-client) 19 | 20 | ## Usage 21 | 22 | There are **two ways to use the client**: 23 | - **Standalone import script** - Single line, standard JS script in your HTML. 24 | - **SDK client** - Import the SDK client into your project and use in any SPA. 25 | 26 | ### Standalone Import Script Usage 27 | 28 | Then include the standalone script in your HTML: 29 | ```html 30 | 31 | ... 32 | 33 | ... 34 | 35 | 36 | 37 | ``` 38 | 39 | You need to replace `` with the origin of your deployed backend. Available attributes on the script 40 | are: 41 | - `site` - Required. The name of your site, this must correspond with the name you specified when deploying the 42 | `serverless-website-analytics` backend. 43 | - `api-url` - Optional. Uses the same origin as the current script if not specified. This is the URL to the backend. 44 | Allowing it to be specified opens a few use cases for testing. 45 | - `attr-tracking` - Optional. If `"true"`, the script will track all `button` and `a` HTML elements that have the 46 | `swa-event` attribute on them. Example: ``. See below for options. 47 | - `serverless-website-analytics` - Optional. This is only required if the browser does not support `document.currentScript` 48 | (All modern browsers since 2015 do). Only specify the tag, no value is needed. 49 | 50 | #### Tracking 51 | 52 | ##### Attribute tracking 53 | 54 | If you specified the `attr-tracking` attribute on the script tag, then all `button` and `a` HTML elements that have the 55 | `swa-event` attribute on them will be tracked. The `swa-event` attribute is required and the following attributes are 56 | available: 57 | - `swa-event-category` - Optional. The category of the event that can be used to group events. 58 | - `swa-event-data` - Optional. The data of the event. Defaults to 1. 59 | - `swa-event-async` - Optional. If present, the event will be sent asynchronously 60 | with no guarantee of delivery but a better chance of not being canceled if page is unloaded right after. 61 | 62 | ```html 63 | 64 | 65 | 66 | 67 | 68 | 69 | Click me 70 | ``` 71 | 72 | ##### Manual tracking 73 | 74 | You can find the instantiated instance of the `serverless-website-analytics` component under the window at `window.swa`. 75 | This enables you to call all the functions like tracking manually. Example: 76 | ```js 77 | window.swa.v1.analyticsTrack('about_click') 78 | ``` 79 | 80 | #### Beacon/pixel tracking 81 | 82 | Beacon/pixel tracking can be used as alternative to HTML attribute tracking. Beacon tracking is useful for 83 | tracking events outside your domain, like email opens, external blog views, etc. 84 | ```html 85 | 86 | ``` 87 | The `site` and `event` fields are required. The `category` field and all the other fields are optional, except 88 | the `referrer` field, which is not supported. 89 | 90 | ### SDK Client Usage 91 | 92 | Install the client: 93 | ``` 94 | npm install serverless-website-analytics-client 95 | ``` 96 | 97 | Irrelevant of the framework, you have to do the following to track page views on your site: 98 | 99 | 1. Initialize the client only once with `analyticsPageInit`. The site name must correspond with one that you specified 100 | when deploying the `serverless-website-analytics` backend. You also need the URL to the backend. Make sure your frontend 101 | site's `Origin` is whitelisted in the backend config. 102 | 2. On each route change call the `analyticsPageChange` function with the name of the new page. 103 | 104 | Beacon/pixel tracking is also supported but it is not recommended as it looses some info the SDK gathers. This includes 105 | the `session_id`, `user_id` and `referrer`fields. The first two can still be specified but the `reffer` field ca not. 106 | 107 | > [!IMPORTANT] 108 | > The following sections show you how to do it in a few frameworks, but you can still DIY with the SDK in **ANY framework**. 109 | > The [OpenAPI spec](https://github.com/rehanvdm/serverless-website-analytics-client/blob/master/package/src/OpenAPI-Ingest.yaml) 110 | > can be used for any language that isn't TS/JS. 111 | 112 | 113 | #### Vue 114 | 115 | [_./usage/vue/vue-project/src/main.ts_](https://github.com/rehanvdm/serverless-website-analytics-client/blob/master/usage/vue/vue-project/src/main.ts) 116 | ```typescript 117 | ... 118 | import * as swaClient from 'serverless-website-analytics-client'; 119 | 120 | const app = createApp(App); 121 | app.use(router); 122 | 123 | swaClient.v1.analyticsPageInit({ 124 | inBrowser: true, //Not SSR 125 | site: "", //vue-project 126 | apiUrl: "", //https://my-serverless-website-analytics-backend.com 127 | // debug: true, 128 | }); 129 | router.afterEach((event) => { 130 | swaClient.v1.analyticsPageChange(event.path); 131 | }); 132 | 133 | app.mount('#app'); 134 | 135 | export { swaClient }; 136 | ``` 137 | 138 | Tracking: 139 | 140 | [_./usage/vue/vue-project/src/App.vue_](https://github.com/rehanvdm/serverless-website-analytics-client/blob/master/usage/vue/vue-project/src/App.vue) 141 | ```typescript 142 | import {swaClient} from "./main"; 143 | ... 144 | // (event: string, data?: number, category?: string) 145 | swaClient.v1.analyticsTrack("vue", count.value, "test") 146 | ``` 147 | 148 | #### React 149 | 150 | [_./usage/react/react-project/src/main.tsx_](https://github.com/rehanvdm/serverless-website-analytics-client/blob/master/usage/react/react-project/src/main.tsx) 151 | ```typescript 152 | ... 153 | import * as swaClient from 'serverless-website-analytics-client'; 154 | 155 | const router = createBrowserRouter([ 156 | ... 157 | ]); 158 | 159 | swaClient.v1.analyticsPageInit({ 160 | inBrowser: true, //Not SSR 161 | site: "", //vue-project 162 | apiUrl: "", //https://my-serverless-website-analytics-backend.com 163 | // debug: true, 164 | }); 165 | 166 | router.subscribe((state) => { 167 | swaClient.v1.analyticsPageChange(state.location.pathname); 168 | }); 169 | swaClient.v1.analyticsPageChange("/"); 170 | 171 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 172 | 173 | 174 | , 175 | ) 176 | 177 | export { swaClient }; 178 | ``` 179 | 180 | Tracking: 181 | 182 | [_./usage/react/react-project/src/App.tsx_](https://github.com/rehanvdm/serverless-website-analytics-client/blob/master/usage/react/react-project/src/App.tsx) 183 | ```typescript 184 | import {swaClient} from "./main.tsx"; 185 | ... 186 | // (event: string, data?: number, category?: string) 187 | swaClient.v1.analyticsTrack("vue", count.value, "test") 188 | ```` 189 | 190 | #### Svelte 191 | 192 | [_./usage/svelte/svelte-project/src/App.svelte_](https://github.com/rehanvdm/serverless-website-analytics-client/blob/master/usage/svelte/svelte-project/src/App.svelte) 193 | ```sveltehtml 194 | 195 | 196 | 197 | 222 | ``` 223 | 224 | Tracking: 225 | 226 | [_./usage/svelte/svelte-project/src/lib/Counter.svelte_](https://github.com/rehanvdm/serverless-website-analytics-client/blob/master/usage/svelte/svelte-project/src/lib/Counter.svelte) 227 | ```sveltehtml 228 | 234 | ```` 235 | 236 | #### Angular 237 | 238 | See example at: https://github.com/cebert/serverless-web-analytics-demo-spa-application 239 | 240 | #### PHP 241 | 242 | See example at: https://github.com/wheeleruniverse/wheelerrecommends 243 | 244 | #### ..Any other framework 245 | 246 | ## Package src 247 | 248 | The src located at `package/src/index.ts` does not use any libraries to generate the API. The TypeScript types however 249 | are generated from the `OpenAPI-Ingest.yaml` file that is copied(manually) from the backend 250 | (`cd src/src && npm run generate-openapi-ingest`). Once the latest `OpenAPI-Ingest.yaml` is copied to the `package/src` 251 | directory the command `cd package && npm run generate-types` can be run to generate the latest TS types. 252 | 253 | The client is written in a functional manner and leverages namespaces for the versioning. 254 | 255 | The deploy scripts are managed by [wireit](https://github.com/google/wireit) which just supercharges your `npm` scripts. 256 | It calls the certain functions in the `package/src/scripts.ts` file to do things like generate the API TS types from the 257 | OpenAPI spec and package the app from the `/src` to the `/dist` directory. 258 | 259 | ## Developing 260 | 261 | Commits MUST follow the [Conventional Commits](https://gist.github.com/Zekfad/f51cb06ac76e2457f11c80ed705c95a3) standard. 262 | Commit names are used to determine the next logical version number as per [semantic-release](https://github.com/semantic-release/semantic-release). 263 | A small cheat sheet, in order to get a: 264 | - Patch - Have at least 1 commit name with a `fix:` type 265 | - Minor - Have at least 1 commit name with a `feat:` type 266 | - Major - Have at least 1 commit message (in the footer) that includes the words: `BREAKING CHANGE: ` 267 | 268 | A new Major version should only be rolled when a new version of the backend ingest API is rolled out. The client package 269 | major version must always match the current ingest API latest version. 270 | 271 | A GitHub workflow is used to create the new version on GitHub and NPM. It is only triggered on the condition that it 272 | is a push to main (after a PR is merged) and that the `/package` files changed. 273 | 274 | ## Contributing 275 | 276 | All contributions are welcome! 277 | 278 | There are currently no test other than manually verifying the code works as expected 279 | by the frameworks as in the Usage section. There is also no style enforced, but I would prefer the following for the 280 | time being: 281 | - 2 spaces 282 | - Semicolons on line-endings 283 | - Braces on new lines for functions, types can be inline. 284 | 285 | I know barbaric 😅. Tests, linting, prettier and pre commit hooks still need to be added and then the style 286 | mention above can be forgo. 287 | 288 | ## FAQ 289 | 290 | ### The network calls to the backend fail with 403 291 | 292 | This is because the `Origin` that is sending the request has not been added to the `ALLOWED_ORIGINS` config. 293 | 294 | Examples of Origins: 295 | - If you are doing local development then your origin might look like `http://localhost:5173` 296 | - If it from your personal blog then it might look like `https://rehanvdm.com` but don't forget to also whitelist all 297 | possible subdomains as well like: `https://www.rehanvdm.com`. 298 | 299 | If this value is currently set to allow all Origins (not recommended) with a `*` then the 403 is caused by something 300 | else and not the `Origin` whitelisting. 301 | 302 | ### Why not fetch with `keepalive` in the client 303 | 304 | The `navigator.sendBeacon(...)` has type `ping` in Chrome or `beacon` in FireFox instead of `POST`, it also does 305 | not send preflight `OPTION` calls even if it is not the same host. Browsers just handle it slightly differently 306 | and the probability of it being delivered is greater than fetch with `keepalive` set. 307 | More on the [topic](https://medium.com/fiverr-engineering/benefits-of-sending-analytical-information-with-sendbeacon-a959cb206a7a). 308 | -------------------------------------------------------------------------------- /package/.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "conventionalcommits", 3 | "branches": ["master"], 4 | "plugins": [ 5 | "@semantic-release/commit-analyzer", 6 | "@semantic-release/release-notes-generator", 7 | ["@semantic-release/changelog", {"changelogTitle": "# Changelog"}], 8 | ["@semantic-release/npm", { 9 | "pkgRoot": "dist" 10 | }], 11 | ["@semantic-release/git", { 12 | "assets": ["CHANGELOG.md"], 13 | "message": "chore: Release ${nextRelease.version} [skip ci]" 14 | }] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /package/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.8.1](https://github.com/rehanvdm/serverless-website-analytics-client/compare/v1.8.0...v1.8.1) (2024-07-21) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * use the element which has the event listener on client-script.js ([#22](https://github.com/rehanvdm/serverless-website-analytics-client/issues/22)) ([ff86b9a](https://github.com/rehanvdm/serverless-website-analytics-client/commit/ff86b9a896ab7b1decbe70ef0773f1ac57ceb0a9)) 9 | 10 | ## [1.8.0](https://github.com/rehanvdm/serverless-website-analytics-client/compare/v1.7.0...v1.8.0) (2024-04-10) 11 | 12 | 13 | ### Features 14 | 15 | * ability to send tracking event asynchronously ([#20](https://github.com/rehanvdm/serverless-website-analytics-client/issues/20)) ([d851057](https://github.com/rehanvdm/serverless-website-analytics-client/commit/d8510579cd79ad6039e9bd1a10ca5a972d02e6cf)) 16 | 17 | ## [1.7.0](https://github.com/rehanvdm/serverless-website-analytics-client/compare/v1.6.1...v1.7.0) (2024-03-18) 18 | 19 | 20 | ### Features 21 | 22 | * add docs about pixel tracking ([#15](https://github.com/rehanvdm/serverless-website-analytics-client/issues/15)) ([9f01d24](https://github.com/rehanvdm/serverless-website-analytics-client/commit/9f01d24b8df6fe91d4234ba3d62aaf8a54d504b5)) 23 | * change send method and allow setting userId ([62599cf](https://github.com/rehanvdm/serverless-website-analytics-client/commit/62599cff586be7d9433e508374ce0e4fddcb91b6)) 24 | 25 | ## [1.6.1](https://github.com/rehanvdm/serverless-website-analytics-client/compare/v1.6.0...v1.6.1) (2023-10-31) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * standalone usage of swaClient ([#14](https://github.com/rehanvdm/serverless-website-analytics-client/issues/14)) ([c11a27f](https://github.com/rehanvdm/serverless-website-analytics-client/commit/c11a27f41d3a8eae69e4b393b36bdb942a813c68)) 31 | 32 | ## [1.6.0](https://github.com/rehanvdm/serverless-website-analytics-client/compare/v1.5.0...v1.6.0) (2023-10-30) 33 | 34 | 35 | ### Features 36 | 37 | * add events [#9](https://github.com/rehanvdm/serverless-website-analytics-client/issues/9) ([#13](https://github.com/rehanvdm/serverless-website-analytics-client/issues/13)) ([6974f19](https://github.com/rehanvdm/serverless-website-analytics-client/commit/6974f19f01634427b3a7a229b3e923103658be59)) 38 | 39 | ## [1.5.0](https://github.com/rehanvdm/serverless-website-analytics-client/compare/v1.4.0...v1.5.0) (2023-06-13) 40 | 41 | 42 | ### Features 43 | 44 | * add standalone usage [#3](https://github.com/rehanvdm/serverless-website-analytics-client/issues/3) ([#10](https://github.com/rehanvdm/serverless-website-analytics-client/issues/10)) ([e32e050](https://github.com/rehanvdm/serverless-website-analytics-client/commit/e32e050543be1fb1ded6e177dcdf3a9e46498cd2)) 45 | 46 | ## [1.4.0](https://github.com/rehanvdm/serverless-website-analytics-client/compare/v1.3.1...v1.4.0) (2023-06-11) 47 | 48 | 49 | ### Features 50 | 51 | * add react usage [#1](https://github.com/rehanvdm/serverless-website-analytics-client/issues/1) ([#6](https://github.com/rehanvdm/serverless-website-analytics-client/issues/6)) ([e2eb46d](https://github.com/rehanvdm/serverless-website-analytics-client/commit/e2eb46dd5b5ae6849442a42ee76836609997fc9b)) 52 | * add svelte usage [#2](https://github.com/rehanvdm/serverless-website-analytics-client/issues/2) ([e718298](https://github.com/rehanvdm/serverless-website-analytics-client/commit/e7182982ceb0abb0104e276d9956086067bb3cb7)) 53 | * add svelte usage [#2](https://github.com/rehanvdm/serverless-website-analytics-client/issues/2) ([#7](https://github.com/rehanvdm/serverless-website-analytics-client/issues/7)) ([7a9fde5](https://github.com/rehanvdm/serverless-website-analytics-client/commit/7a9fde5e289f5456901ab08137727e062ef5af20)) 54 | * remove unnecessary dependencies from the final package.json [#4](https://github.com/rehanvdm/serverless-website-analytics-client/issues/4) ([df7a36e](https://github.com/rehanvdm/serverless-website-analytics-client/commit/df7a36ecb006ecbf4e316519fb0bf5d22d28a0b6)) 55 | * remove unnecessary dependencies from the final package.json [#4](https://github.com/rehanvdm/serverless-website-analytics-client/issues/4) ([0519b16](https://github.com/rehanvdm/serverless-website-analytics-client/commit/0519b1685d257ade29d7966f3a2e8172eb1a2f31)) 56 | 57 | ## [1.3.1](https://github.com/rehanvdm/serverless-website-analytics-client/compare/v1.3.0...v1.3.1) (2023-05-30) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * update package json ([d87cb66](https://github.com/rehanvdm/serverless-website-analytics-client/commit/d87cb6633a13e76e509c52bb9f98efa2c8b56d9f)) 63 | 64 | ## [1.3.0](https://github.com/rehanvdm/serverless-website-analytics-client-development/compare/v1.2.0...v1.3.0) (2023-05-02) 65 | 66 | 67 | ### Features 68 | 69 | * release ([5b7798a](https://github.com/rehanvdm/serverless-website-analytics-client-development/commit/5b7798a08a6e9a69a7c59a3034096bfb568942dc)) 70 | * release ([8ba3517](https://github.com/rehanvdm/serverless-website-analytics-client-development/commit/8ba35170646cd3532a43eb8158627a040c1d6946)) 71 | 72 | ## [1.2.0](https://github.com/rehanvdm/serverless-website-analytics-client-development/compare/v1.1.0...v1.2.0) (2023-02-27) 73 | 74 | 75 | ### Features 76 | 77 | * change init and cleanup ([4f22694](https://github.com/rehanvdm/serverless-website-analytics-client-development/commit/4f22694a67a333b4e146d5da2c0683292b95256e)) 78 | 79 | ## [1.1.0](https://github.com/rehanvdm/serverless-website-analytics-client-development/compare/v1.0.0...v1.1.0) (2023-02-26) 80 | 81 | 82 | ### Features 83 | 84 | * fix packaing for esm and cjs ([b199c29](https://github.com/rehanvdm/serverless-website-analytics-client-development/commit/b199c295239c89ceca0aa6df44b67f3a25e2c05b)) 85 | 86 | ## 1.0.0 (2023-02-26) 87 | 88 | 89 | ### Features 90 | 91 | * Init ([a8ab9ba](https://github.com/rehanvdm/serverless-website-analytics-client-development/commit/a8ab9bad17944f9ee17551d336b8bcc19402472d)) 92 | * Init ([0fc0fc1](https://github.com/rehanvdm/serverless-website-analytics-client-development/commit/0fc0fc1bea4923201fec7c61f0e64869d5ed966a)) 93 | * Init ([d0eae93](https://github.com/rehanvdm/serverless-website-analytics-client-development/commit/d0eae93183b3ed79af51b9a88780969790df5e93)) 94 | * Init ([d20b41e](https://github.com/rehanvdm/serverless-website-analytics-client-development/commit/d20b41eb5784dc3b4a01f518bf79f64443bcb018)) 95 | * Init ([11c265e](https://github.com/rehanvdm/serverless-website-analytics-client-development/commit/11c265e88f07d79cb52f4e36e0530c7dc2ccc8a2)) 96 | -------------------------------------------------------------------------------- /package/esbuild-runner.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: "transform", 3 | esbuild: { 4 | }, 5 | } -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-website-analytics-client", 3 | "version": "0.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/rehanvdm/serverless-website-analytics-client.git" 7 | }, 8 | "description": "Web client for serverless-website-analytics", 9 | "author": "Rehan van der Merwe (https://twitter.com/der_rehan)", 10 | "license": "GPL-2.0-only", 11 | "main": "index.js", 12 | "module": "index.mjs", 13 | "types": "index.d.ts", 14 | "engines": { 15 | "npm": ">=9.0.0", 16 | "node": ">=18.0.0" 17 | }, 18 | "scripts": { 19 | "generate-api-types": "wireit", 20 | "package": "wireit" 21 | }, 22 | "wireit": { 23 | "generate-api-types": { 24 | "command": "esr scripts.ts -c generate-api-types", 25 | "files": [ 26 | "src/OpenAPI-Ingest.yaml" 27 | ], 28 | "output": [ 29 | "ingest-api-types.ts" 30 | ] 31 | }, 32 | "package": { 33 | "command": "esr scripts.ts -c package", 34 | "files": [ 35 | "scripts.ts", 36 | "src/**/*.ts", 37 | "OpenAPI-Ingest.yaml" 38 | ], 39 | "output": [ 40 | "dist/**" 41 | ] 42 | } 43 | }, 44 | "dependencies": { 45 | "@rollup/plugin-node-resolve": "^15.0.1", 46 | "@rollup/plugin-terser": "^0.4.0", 47 | "@rollup/plugin-typescript": "^11.0.0", 48 | "@semantic-release/changelog": "6.0.1", 49 | "@semantic-release/commit-analyzer": "9.0.2", 50 | "@semantic-release/git": "10.0.1", 51 | "@semantic-release/github": "8.0.6", 52 | "@semantic-release/npm": "^9.0.2", 53 | "@semantic-release/release-notes-generator": "10.0.3", 54 | "@types/node": "^18.14.0", 55 | "@types/yargs": "^17.0.22", 56 | "conventional-changelog-conventionalcommits": "^5.0.0", 57 | "esbuild": "^0.17.10", 58 | "esbuild-runner": "^2.2.2", 59 | "execa": "5.1.1", 60 | "nanoid": "^4.0.1", 61 | "rollup": "^3.17.3", 62 | "rollup-plugin-typescript2": "^0.34.1", 63 | "semantic-release": "19.0.5", 64 | "swagger-typescript-api": "^12.0.3", 65 | "typescript": "^4.9.5", 66 | "wireit": "^0.9.5", 67 | "yargs": "^17.7.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /package/scripts.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import yargs from "yargs" 3 | import {hideBin} from "yargs/helpers" 4 | import { generateApi } from "swagger-typescript-api"; 5 | import fs from "fs/promises"; 6 | import { rollup } from 'rollup'; 7 | import {command as execaCommand, ExecaReturnValue} from "execa"; 8 | import nodeResolve from "@rollup/plugin-node-resolve"; 9 | import typescript from '@rollup/plugin-typescript'; 10 | import terser from "@rollup/plugin-terser"; 11 | 12 | 13 | const paths = { 14 | localPackages: path.resolve(__dirname + "/node_modules/.bin"), 15 | topLevelDir: path.resolve(__dirname+"./../"), 16 | workingDir: path.resolve(__dirname), 17 | src: path.resolve(__dirname+"/src"), 18 | srcInput: path.resolve(__dirname+"/src/index.ts"), 19 | srcInputCdn: path.resolve(__dirname+"/src/cdn/client-script.ts"), 20 | openApiSpec: path.resolve(__dirname+"/src/OpenAPI-Ingest.yaml"), 21 | dist: path.resolve(__dirname+"/dist"), 22 | distCdn: path.resolve(__dirname+"/dist/cdn"), 23 | } 24 | 25 | 26 | const commands = ['package', 'generate-api-types'] as const; 27 | export type Command = typeof commands[number]; 28 | 29 | const argv = yargs(hideBin(process.argv)) 30 | .option('command', { 31 | alias: 'c', 32 | describe: 'the command you want to run', 33 | choices: commands 34 | }) 35 | .demandOption(['c']) 36 | .argv as any; 37 | 38 | 39 | 40 | 41 | (async () => 42 | { 43 | const command = argv.c as Command; 44 | switch (command) 45 | { 46 | case "generate-api-types": 47 | await generateApiTypes(); 48 | break; 49 | case "package": 50 | await build(); 51 | break; 52 | 53 | } 54 | })(); 55 | 56 | async function generateApiTypes() 57 | { 58 | console.time("* GENERATE API TYPES"); 59 | await generateApi({ 60 | name: "ingest-api-types.yaml", 61 | output: paths.src, 62 | input: paths.openApiSpec, 63 | silent: true, 64 | generateClient: false, 65 | generateRouteTypes: true, 66 | generateResponses: true, 67 | extractRequestParams: true, 68 | extractRequestBody: true, 69 | moduleNameFirstTag: true, 70 | }); 71 | console.timeEnd("* GENERATE API TYPES"); 72 | } 73 | 74 | 75 | export async function fileExists(path: string){ 76 | return fs.open(path, 'r').then(async (file) => { await file.close(); return true; }).catch(err => false) 77 | } 78 | export async function folderExists(path: string){ 79 | return fs.opendir(path).then(async (dir) => { await dir.close(); return true; }).catch(err => false) 80 | } 81 | 82 | async function transpile(srcInput: string, dist: string, generateTypes: boolean) { 83 | console.log("*** Clean: " +dist); 84 | let packageFolderExist = await folderExists(dist); 85 | if(!packageFolderExist) //create 86 | await fs.mkdir(dist,{ recursive: true }); 87 | else //clear contents and recreate 88 | { 89 | await fs.rm(dist,{ recursive: true }); 90 | await fs.mkdir(dist,{ recursive: true }); 91 | } 92 | 93 | console.log("*** Transpiling: " +srcInput); 94 | const bundle = await rollup({ 95 | input: srcInput, 96 | plugins: [ 97 | typescript({}), 98 | nodeResolve({ 99 | browser: true, 100 | }), 101 | terser({sourceMap: true}), 102 | ] 103 | }); 104 | 105 | const fileName = path.basename(srcInput, path.extname(srcInput)); 106 | const distOutputCjs = dist + "/"+fileName+".js"; 107 | const distOutputEsm = dist + "/"+fileName+".mjs"; 108 | //@ts-ignore because sourcemap is specified correctly https://github.com/terser/terser#source-map-options 109 | await bundle.write({file: distOutputCjs, format: 'cjs', sourcemap: {filename: "out.js", url: "out.js.map"}}); 110 | //@ts-ignore because sourcemap is specified correctly https://github.com/terser/terser#source-map-options 111 | await bundle.write({file: distOutputEsm, format: 'esm', sourcemap: {filename: "out.mjs", url: "out.mjs.map"}}); 112 | 113 | if(generateTypes) 114 | { 115 | console.log("*** Generate types - d.ts"); 116 | await execaCommand("tsc --declaration --emitDeclarationOnly " + srcInput + " " + 117 | "--outDir " + dist, {reject: true}); 118 | } 119 | } 120 | 121 | async function build() 122 | { 123 | console.time("* BUILD"); 124 | console.log("* BUILD"); 125 | 126 | console.log("** Transpiling.."); 127 | await transpile(paths.srcInput, paths.dist, true,); 128 | await transpile(paths.srcInputCdn, paths.distCdn, false); 129 | 130 | console.log("** Coping files.."); 131 | await fs.copyFile(paths.workingDir+"/package.json", paths.dist+"/package.json"); 132 | 133 | // Read the package.json that will be published and remove some stuff 134 | let packageJson = JSON.parse((await fs.readFile(paths.dist+"/package.json")).toString()); 135 | delete packageJson.dependencies; 136 | delete packageJson.scripts; 137 | delete packageJson.wireit; 138 | await fs.writeFile(paths.dist+"/package.json", JSON.stringify(packageJson, null, 2)); 139 | 140 | await fs.copyFile(paths.topLevelDir+"/README.md", paths.dist+"/README.md"); 141 | 142 | console.timeEnd("* BUILD"); 143 | } 144 | 145 | -------------------------------------------------------------------------------- /package/src/OpenAPI-Ingest.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Serverless Website Analytics Ingest API 4 | version: '-' 5 | servers: 6 | - url: '-' 7 | paths: 8 | /v1/page/view: 9 | post: 10 | operationId: mutation.pageView 11 | requestBody: 12 | required: true 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | site: 19 | type: string 20 | user_id: 21 | type: string 22 | session_id: 23 | type: string 24 | page_id: 25 | type: string 26 | page_url: 27 | type: string 28 | page_opened_at: 29 | type: string 30 | time_on_page: 31 | type: number 32 | utm_source: 33 | type: string 34 | utm_medium: 35 | type: string 36 | utm_campaign: 37 | type: string 38 | utm_term: 39 | type: string 40 | utm_content: 41 | type: string 42 | querystring: 43 | type: string 44 | referrer: 45 | type: string 46 | required: 47 | - site 48 | - user_id 49 | - session_id 50 | - page_id 51 | - page_url 52 | - page_opened_at 53 | - time_on_page 54 | additionalProperties: false 55 | parameters: [] 56 | responses: 57 | '200': 58 | description: Successful response 59 | content: 60 | application/json: 61 | schema: {} 62 | default: 63 | $ref: '#/components/responses/error' 64 | /v1/event/track: 65 | post: 66 | operationId: mutation.eventTrack 67 | requestBody: 68 | required: true 69 | content: 70 | application/json: 71 | schema: 72 | type: object 73 | properties: 74 | site: 75 | type: string 76 | user_id: 77 | type: string 78 | session_id: 79 | type: string 80 | category: 81 | type: string 82 | event: 83 | type: string 84 | tracked_at: 85 | type: string 86 | data: 87 | type: number 88 | utm_source: 89 | type: string 90 | utm_medium: 91 | type: string 92 | utm_campaign: 93 | type: string 94 | utm_term: 95 | type: string 96 | utm_content: 97 | type: string 98 | querystring: 99 | type: string 100 | referrer: 101 | type: string 102 | required: 103 | - site 104 | - user_id 105 | - session_id 106 | - event 107 | - tracked_at 108 | additionalProperties: false 109 | parameters: [] 110 | responses: 111 | '200': 112 | description: Successful response 113 | content: 114 | application/json: 115 | schema: {} 116 | default: 117 | $ref: '#/components/responses/error' 118 | components: 119 | securitySchemes: 120 | Authorization: 121 | type: http 122 | scheme: bearer 123 | responses: 124 | error: 125 | description: Error response 126 | content: 127 | application/json: 128 | schema: 129 | type: object 130 | properties: 131 | message: 132 | type: string 133 | code: 134 | type: string 135 | issues: 136 | type: array 137 | items: 138 | type: object 139 | properties: 140 | message: 141 | type: string 142 | required: 143 | - message 144 | additionalProperties: false 145 | required: 146 | - message 147 | - code 148 | additionalProperties: false 149 | -------------------------------------------------------------------------------- /package/src/cdn/client-script.ts: -------------------------------------------------------------------------------- 1 | import * as swaClient from "../"; 2 | 3 | (() => { 4 | /* Only specify the data tag serverless-website-analytics if the browser does not support `document.currentScript` 5 | * All modern browsers since 2015 do */ 6 | const me = document.currentScript || document.querySelector('script[serverless-website-analytics]'); 7 | if(!me) 8 | throw new Error("Could not find script tag with attribute 'serverless-website-analytics' on the script tag."); 9 | 10 | const site = me.getAttribute('site'); 11 | if(!site) 12 | throw new Error("Could not find attribute 'site' on the script tag."); 13 | 14 | /* The API URL to send the metrics to will 99% be the same as where the standalone script is loaded from, only use 15 | * the `api-url` if specified explicitly. */ 16 | let scriptOrigin = ""; 17 | try { 18 | //@ts-ignore 19 | scriptOrigin = new URL(me.src).origin; 20 | } catch (e) { 21 | console.error("Could not parse URL from script tag", e); 22 | } 23 | let apiUrl = me.getAttribute('api-url'); 24 | if(!scriptOrigin && !apiUrl) 25 | throw new Error("Could not auto-detect script origin, specify the 'api-url' attribute on the script tag."); 26 | if(!apiUrl) 27 | apiUrl = scriptOrigin; 28 | 29 | const routing = me.getAttribute('routing') 30 | if(routing && (routing !== "path" && routing !== "hash")) 31 | throw new Error("Attribute 'routing' must be either 'path' or 'hash'"); 32 | 33 | function getPath() { 34 | if(routing === "path") 35 | return window.location.pathname; 36 | else if(routing === "hash") 37 | return window.location.hash ? window.location.hash : "/"; 38 | else 39 | return window.location.pathname + (window.location.hash ? "#" + window.location.hash : ""); 40 | } 41 | 42 | //@ts-ignore 43 | window.swa = swaClient; 44 | swaClient.v1.analyticsPageInit({ 45 | inBrowser: true, 46 | site: site, 47 | apiUrl: apiUrl, 48 | // debug: true, 49 | }); 50 | 51 | let currentPage = location.href; 52 | setInterval(function() { 53 | if (currentPage != location.href) { 54 | currentPage = location.href; 55 | // console.log('New URL:', getPath()); 56 | swaClient.v1.analyticsPageChange(getPath()); 57 | } 58 | }, 500); 59 | 60 | swaClient.v1.analyticsPageChange(getPath()); 61 | 62 | /* Track all `button` and `a` HTML elements that have the `swa-event` attribute if `attr-tracking` is specified on the script 63 | * Example: 64 | * 65 | * 66 | * 67 | * Click me 68 | * */ 69 | const attrTracking = me.getAttribute('attr-tracking'); 70 | if(attrTracking && attrTracking != "false") { 71 | document.querySelectorAll('button, a').forEach(function(element) { 72 | element.addEventListener('click', (event) => { 73 | if(!event.currentTarget) 74 | return; 75 | 76 | const eventTarget = event.currentTarget as HTMLElement; 77 | const trackEvent = eventTarget.getAttribute('swa-event'); 78 | if(trackEvent) { 79 | const trackCategory = eventTarget.getAttribute('swa-event-category') || undefined; 80 | const trackData = eventTarget.getAttribute('swa-event-data') || undefined; 81 | const trackDataNumber = trackData ? Number(trackData) : undefined; 82 | const asyncAttr = eventTarget.getAttribute('swa-event-async'); 83 | const isAsync = (asyncAttr !== null && asyncAttr.toLowerCase() !== "false") || false; 84 | 85 | // Have to use on the window object because the local defined swaClient is out of scope, it seems. 86 | //@ts-ignore 87 | const globalSwaClient = window.swa as typeof swaClient; 88 | globalSwaClient.v1.analyticsTrack(trackEvent, trackDataNumber, trackCategory, isAsync); 89 | } 90 | }); 91 | }); 92 | } 93 | 94 | 95 | })(); 96 | 97 | -------------------------------------------------------------------------------- /package/src/index.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from "nanoid"; 2 | import * as ApiTypes from "./ingest-api-types"; 3 | 4 | export namespace v1 5 | { 6 | const storagePrefix = "swa-"; 7 | const STORAGE_KEYS = { 8 | LOCAL: { 9 | USER_ID: storagePrefix+"userId", 10 | }, 11 | }; 12 | 13 | const pathTrackPage = "/api-ingest/v1/page/view"; 14 | const pathTrackEvent = "/api-ingest/v1/event/track"; 15 | 16 | /* === SESSION STORAGE === */ 17 | let sessionId: string | undefined = undefined; 18 | let userId: string | undefined = undefined; 19 | let currentPageAnalytic: ApiTypes.V1.MutationPageView.RequestBody | undefined; 20 | let pageTimeIncrementStarted: boolean = false; 21 | let visibilityListening: boolean = false; 22 | 23 | let global: { 24 | inBrowser: boolean, 25 | site: string, 26 | apiUrl: string, 27 | debug: boolean, 28 | } = { 29 | inBrowser: false, 30 | site: "", 31 | apiUrl: "", 32 | debug: false 33 | }; 34 | /* ======================= */ 35 | 36 | /** 37 | * Send the request to the server. If bestEffort is true, we use fetch with keepalive for all browsers except Firefox 38 | * that uses navigator.sendBeacon, otherwise we uses fetch (which is guaranteed deliver) 39 | * @param urlAndPath 40 | * @param jsonDataStringified 41 | * @param bestEffort 42 | * @private 43 | */ 44 | function sendRequest(urlAndPath: string, jsonDataStringified: string, bestEffort: boolean = false) 45 | { 46 | if(bestEffort) 47 | { 48 | const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; 49 | if (isFirefox) 50 | navigator.sendBeacon(urlAndPath, jsonDataStringified); // Blocked by add blockers unless on the same domain 51 | else { 52 | fetch(urlAndPath, { 53 | keepalive: true, // Similar to navigator.sendBeacon(..) but not supported in FF 54 | method: 'POST', 55 | headers: { 56 | 'content-type': 'application/json', 57 | }, 58 | body: jsonDataStringified, 59 | }); 60 | } 61 | } 62 | else 63 | { 64 | fetch(urlAndPath, { 65 | method: 'POST', 66 | headers: { 67 | 'content-type': 'application/json', 68 | }, 69 | body: jsonDataStringified, 70 | }); 71 | } 72 | } 73 | 74 | 75 | type InitOptions = { 76 | /** 77 | * If SSR set this to false 78 | */ 79 | inBrowser: boolean, 80 | 81 | /** 82 | * The site name to send analytics as 83 | */ 84 | site: string, 85 | 86 | /** 87 | * The API URL 88 | */ 89 | apiUrl: string, 90 | 91 | /** 92 | * Prints debug messages if enabled 93 | */ 94 | debug?: boolean 95 | 96 | /** 97 | * The user id, if not set, a random one will be generated and stored in local storage for subsequent visits 98 | */ 99 | userId?: string 100 | } 101 | 102 | /** 103 | * Init the client only once 104 | * @param initOptions 105 | */ 106 | export function analyticsPageInit(initOptions: InitOptions) 107 | { 108 | global = { 109 | inBrowser: initOptions.inBrowser, 110 | site: initOptions.site, 111 | apiUrl: initOptions.apiUrl, 112 | debug: !!initOptions.debug 113 | }; 114 | 115 | if(!global.inBrowser) 116 | return; 117 | 118 | if(!sessionId) 119 | sessionId = nanoid(); 120 | else 121 | throw new Error("Analytics has already been initialized, `analyticsPageInit` must only be called once"); 122 | 123 | if(initOptions.userId) { 124 | userId = initOptions.userId; 125 | } 126 | else { 127 | userId = localStorage.getItem(STORAGE_KEYS.LOCAL.USER_ID) || undefined; 128 | if(!userId) 129 | { 130 | userId = nanoid(); 131 | localStorage.setItem(STORAGE_KEYS.LOCAL.USER_ID, userId); 132 | } 133 | } 134 | 135 | 136 | /* Timer that starts once and will get the current analytic page object and increment the time only if the page is visible */ 137 | if(!pageTimeIncrementStarted) 138 | { 139 | global.debug && console.log("pageTimeIncrementStarted"); 140 | pageTimeIncrementStarted = true; 141 | 142 | setInterval(() => 143 | { 144 | if (document.visibilityState === 'visible') 145 | { 146 | if(currentPageAnalytic) 147 | currentPageAnalytic.time_on_page++; 148 | 149 | global.debug && currentPageAnalytic && console.log(currentPageAnalytic.page_url, currentPageAnalytic.time_on_page); 150 | } 151 | }, 1000) 152 | } 153 | 154 | /* Send the analytic current objet as soon as the page is not visible anymore, so above sends between page changes 155 | * and this covers the case for the last page navigation that won't trigger the watch event */ 156 | if(!visibilityListening) 157 | { 158 | visibilityListening = true; 159 | //Ehh.. This seems to fire/be hidden on the first load, so sends current analytic then, but not that bad, not gonna spend time to fix now 160 | document.addEventListener('visibilitychange', () => 161 | { 162 | if(document.visibilityState === 'visible') 163 | { 164 | global.debug && console.log("VISIBLE", new Date()); 165 | } 166 | if(document.visibilityState === 'hidden') 167 | { 168 | global.debug && console.log("HIDDEN", new Date()); 169 | if(currentPageAnalytic) 170 | sendRequest(global.apiUrl+pathTrackPage, JSON.stringify(currentPageAnalytic), true); 171 | } 172 | }); 173 | } 174 | } 175 | 176 | function getCleanQueryString(queryStringParams: { [p: string]: string }) { 177 | const removeFromParams = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]; 178 | const remainderQs = []; 179 | for (let key of Object.keys(queryStringParams)) { 180 | if (removeFromParams.includes(key)) 181 | continue; 182 | 183 | remainderQs.push(key + "=" + queryStringParams[key]); 184 | } 185 | if (remainderQs.length == 0) 186 | return undefined; 187 | 188 | return remainderQs.join(","); 189 | } 190 | 191 | /** 192 | * Call on every page change 193 | * @param path The new page path 194 | */ 195 | export function analyticsPageChange(path: string) 196 | { 197 | if(!global.inBrowser) 198 | return; 199 | 200 | if(!sessionId || !userId) 201 | throw new Error("Analytics have not been initialized"); 202 | 203 | /* If we have an existing page analytic send it (because we incremented the time on page for it), before creating the new one */ 204 | if(currentPageAnalytic) 205 | { 206 | global.debug && console.log("Sending previous analytic"); 207 | sendRequest(global.apiUrl+pathTrackPage, JSON.stringify(currentPageAnalytic)); 208 | } 209 | 210 | const pageId = nanoid(); 211 | const openedAt = (new Date()).toISOString(); 212 | 213 | /* Get querystring params */ 214 | const urlSearchParams = new URLSearchParams(window.location.search); 215 | const params = Object.fromEntries(urlSearchParams.entries()); 216 | 217 | currentPageAnalytic = { 218 | site: global.site, 219 | user_id: userId, 220 | session_id: sessionId, 221 | page_id: pageId, 222 | page_url: path, 223 | page_opened_at: openedAt, 224 | time_on_page: 0, 225 | utm_source: params.utm_source, 226 | utm_medium: params.utm_medium, 227 | utm_campaign: params.utm_campaign, 228 | utm_term: params.utm_term, 229 | utm_content: params.utm_content, 230 | referrer: document.referrer, 231 | querystring: getCleanQueryString(params) 232 | }; 233 | 234 | 235 | 236 | global.debug && console.log("Sending new analytic"); 237 | sendRequest(global.apiUrl+pathTrackPage, JSON.stringify(currentPageAnalytic)); 238 | } 239 | 240 | 241 | /** 242 | * Call on every event you want to track, like button clicks etc 243 | * @param event The event name 244 | * @param data If omitted, defaults to 1 245 | * @param category Optional 246 | * @param isAsync Make a request with no guarantee of delivery but a better chance of not being canceled if page is unloaded right after. Defaults to false. 247 | */ 248 | export function analyticsTrack(event: string, data?: number, category?: string, isAsync: boolean = false) 249 | { 250 | if(!global.inBrowser) 251 | return; 252 | 253 | if(!sessionId || !userId) 254 | throw new Error("Analytics have not been initialized"); 255 | 256 | const trackedAt = (new Date()).toISOString(); 257 | 258 | /* Get querystring params */ 259 | const urlSearchParams = new URLSearchParams(window.location.search); 260 | const params = Object.fromEntries(urlSearchParams.entries()); 261 | 262 | const trackedEvent: ApiTypes.V1.MutationEventTrack.RequestBody = { 263 | site: global.site, 264 | user_id: userId!, 265 | session_id: sessionId!, 266 | category: category, 267 | event: event, 268 | data: data, 269 | tracked_at: trackedAt, 270 | utm_source: params.utm_source, 271 | utm_medium: params.utm_medium, 272 | utm_campaign: params.utm_campaign, 273 | utm_term: params.utm_term, 274 | utm_content: params.utm_content, 275 | referrer: document.referrer, 276 | querystring: getCleanQueryString(params) 277 | }; 278 | 279 | sendRequest(global.apiUrl+pathTrackEvent, JSON.stringify(trackedEvent), isAsync); 280 | } 281 | 282 | } 283 | -------------------------------------------------------------------------------- /package/src/ingest-api-types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /* 4 | * --------------------------------------------------------------- 5 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 6 | * ## ## 7 | * ## AUTHOR: acacode ## 8 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 9 | * --------------------------------------------------------------- 10 | */ 11 | 12 | export interface MutationPageViewPayload { 13 | site: string; 14 | user_id: string; 15 | session_id: string; 16 | page_id: string; 17 | page_url: string; 18 | page_opened_at: string; 19 | time_on_page: number; 20 | utm_source?: string; 21 | utm_medium?: string; 22 | utm_campaign?: string; 23 | utm_term?: string; 24 | utm_content?: string; 25 | querystring?: string; 26 | referrer?: string; 27 | } 28 | 29 | export interface MutationEventTrackPayload { 30 | site: string; 31 | user_id: string; 32 | session_id: string; 33 | category?: string; 34 | event: string; 35 | tracked_at: string; 36 | data?: number; 37 | utm_source?: string; 38 | utm_medium?: string; 39 | utm_campaign?: string; 40 | utm_term?: string; 41 | utm_content?: string; 42 | querystring?: string; 43 | referrer?: string; 44 | } 45 | 46 | export namespace V1 { 47 | /** 48 | * No description 49 | * @name MutationPageView 50 | * @request POST:/v1/page/view 51 | * @response `200` `any` Successful response 52 | * @response `default` `{ 53 | message: string, 54 | code: string, 55 | issues?: ({ 56 | message: string, 57 | 58 | })[], 59 | 60 | }` 61 | */ 62 | export namespace MutationPageView { 63 | export type RequestParams = {}; 64 | export type RequestQuery = {}; 65 | export type RequestBody = MutationPageViewPayload; 66 | export type RequestHeaders = {}; 67 | export type ResponseBody = any; 68 | } /** 69 | * No description 70 | * @name MutationEventTrack 71 | * @request POST:/v1/event/track 72 | * @response `200` `any` Successful response 73 | * @response `default` `{ 74 | message: string, 75 | code: string, 76 | issues?: ({ 77 | message: string, 78 | 79 | })[], 80 | 81 | }` 82 | */ 83 | export namespace MutationEventTrack { 84 | export type RequestParams = {}; 85 | export type RequestQuery = {}; 86 | export type RequestBody = MutationEventTrackPayload; 87 | export type RequestHeaders = {}; 88 | export type ResponseBody = any; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": [ 6 | "ES2018", 7 | "DOM" 8 | ], 9 | "moduleResolution": "Node", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "noImplicitThis": true, 15 | "alwaysStrict": true, 16 | "noUnusedLocals": false, 17 | "noUnusedParameters": false, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": false, 20 | "inlineSourceMap": true, 21 | "inlineSources": true, 22 | "experimentalDecorators": true, 23 | "strictPropertyInitialization": false, 24 | "typeRoots": [ 25 | "./node_modules/@types" 26 | ] 27 | }, 28 | "exclude": [ 29 | "node_modules", 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /usage/react/react-project/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /usage/react/react-project/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /usage/react/react-project/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /usage/react/react-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "localforage": "^1.10.0", 14 | "match-sorter": "^6.3.1", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-router-dom": "^6.12.1", 18 | "serverless-website-analytics-client": "^1.3.1", 19 | "sort-by": "^1.2.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.0.37", 23 | "@types/react-dom": "^18.0.11", 24 | "@typescript-eslint/eslint-plugin": "^5.59.0", 25 | "@typescript-eslint/parser": "^5.59.0", 26 | "@vitejs/plugin-react": "^4.0.0", 27 | "eslint": "^8.38.0", 28 | "eslint-plugin-react-hooks": "^4.6.0", 29 | "eslint-plugin-react-refresh": "^0.3.4", 30 | "typescript": "^5.0.2", 31 | "vite": "^4.3.9" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /usage/react/react-project/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /usage/react/react-project/src/About.tsx: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | import {Link} from "react-router-dom"; 3 | 4 | function About() { 5 | return ( 6 | <> 7 |

About

8 |

Some about stuff

9 |

Click to go to the Home page

10 | 11 | ) 12 | } 13 | 14 | export default About 15 | -------------------------------------------------------------------------------- /usage/react/react-project/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /usage/react/react-project/src/App.tsx: -------------------------------------------------------------------------------- 1 | // import { useState } from 'react' 2 | import reactLogo from './assets/react.svg' 3 | import viteLogo from '/vite.svg' 4 | import './App.css' 5 | import {Link} from "react-router-dom"; 6 | import {useState} from "react"; 7 | import {swaClient} from "./main.tsx"; 8 | 9 | function App() { 10 | const [count, setCount] = useState(0) 11 | 12 | function handleClick() { 13 | console.log('Click happened'); 14 | setCount((count) => count + 1) 15 | swaClient.v1.analyticsTrack("react", count, "test") 16 | } 17 | 18 | return ( 19 | <> 20 | 28 |

Vite + React

29 |
30 |

Click to go to the About page

31 | 34 |
35 | 36 | 37 | ) 38 | } 39 | 40 | export default App 41 | -------------------------------------------------------------------------------- /usage/react/react-project/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /usage/react/react-project/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /usage/react/react-project/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import About from './About.tsx' 5 | import { 6 | createBrowserRouter, 7 | RouterProvider, 8 | } from "react-router-dom"; 9 | import './index.css' 10 | import * as swaClient from 'serverless-website-analytics-client'; 11 | // import * as swaClient from '../../../../package/src/index.ts'; 12 | 13 | const router = createBrowserRouter([{ 14 | path: "/", 15 | element: 16 | }, { 17 | path: "/about", 18 | element: 19 | }, 20 | ]); 21 | 22 | swaClient.v1.analyticsPageInit({ 23 | inBrowser: true, 24 | site: "tests", 25 | apiUrl: "http://localhost:3000", 26 | // apiUrl: "https://d3nhr87nci4rd5.cloudfront.net", 27 | // debug: true, 28 | }); 29 | 30 | router.subscribe((state) => { 31 | swaClient.v1.analyticsPageChange(state.location.pathname); 32 | }); 33 | swaClient.v1.analyticsPageChange("/"); 34 | 35 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 36 | 37 | 38 | , 39 | ) 40 | 41 | export { swaClient }; 42 | -------------------------------------------------------------------------------- /usage/react/react-project/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /usage/react/react-project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /usage/react/react-project/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /usage/react/react-project/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /usage/standalone/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | About 6 | 7 | 8 |

Test for Standalone - About

9 | 10 |
11 |
12 | 13 |

Click for the Home page

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /usage/standalone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Home 6 | 7 | 8 |

Test for Standalone - Home

9 | 10 |
11 |
12 | 13 |

Click for the About page

14 |

Click for the About page

15 |

Click for the About page

16 |

Click for the Same page with hash page

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/README.md: -------------------------------------------------------------------------------- 1 | # Svelte + TS + Vite 2 | 3 | This template should help get you started developing with Svelte and TypeScript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). 8 | 9 | ## Need an official Svelte framework? 10 | 11 | Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. 12 | 13 | ## Technical considerations 14 | 15 | **Why use this over SvelteKit?** 16 | 17 | - It brings its own routing solution which might not be preferable for some users. 18 | - It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. 19 | 20 | This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. 21 | 22 | Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. 23 | 24 | **Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** 25 | 26 | Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. 27 | 28 | **Why include `.vscode/extensions.json`?** 29 | 30 | Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. 31 | 32 | **Why enable `allowJs` in the TS template?** 33 | 34 | While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. 35 | 36 | **Why is HMR not preserving my local component state?** 37 | 38 | HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). 39 | 40 | If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. 41 | 42 | ```ts 43 | // store.ts 44 | // An extremely simple external store 45 | import { writable } from 'svelte/store' 46 | export default writable(0) 47 | ``` 48 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Svelte + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json" 11 | }, 12 | "devDependencies": { 13 | "@sveltejs/vite-plugin-svelte": "^2.0.4", 14 | "@tsconfig/svelte": "^4.0.1", 15 | "svelte": "^3.58.0", 16 | "svelte-check": "^3.3.1", 17 | "tslib": "^2.5.0", 18 | "typescript": "^5.0.2", 19 | "vite": "^4.3.9" 20 | }, 21 | "dependencies": { 22 | "serverless-website-analytics-client": "^1.3.1", 23 | "svelte-spa-router": "^3.3.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/public/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/src/About.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

About

7 |

Some about stuff

8 |

Click to go to the Home page

9 | 10 |
11 | 12 | 29 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/src/App.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/src/Home.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 16 |

Vite + Svelte

17 | 18 |

Click to go to the About page

19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 | 43 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/src/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | .card { 40 | padding: 2em; 41 | } 42 | 43 | #app { 44 | max-width: 1280px; 45 | margin: 0 auto; 46 | padding: 2rem; 47 | text-align: center; 48 | } 49 | 50 | button { 51 | border-radius: 8px; 52 | border: 1px solid transparent; 53 | padding: 0.6em 1.2em; 54 | font-size: 1em; 55 | font-weight: 500; 56 | font-family: inherit; 57 | background-color: #1a1a1a; 58 | cursor: pointer; 59 | transition: border-color 0.25s; 60 | } 61 | button:hover { 62 | border-color: #646cff; 63 | } 64 | button:focus, 65 | button:focus-visible { 66 | outline: 4px auto -webkit-focus-ring-color; 67 | } 68 | 69 | @media (prefers-color-scheme: light) { 70 | :root { 71 | color: #213547; 72 | background-color: #ffffff; 73 | } 74 | a:hover { 75 | color: #747bff; 76 | } 77 | button { 78 | background-color: #f9f9f9; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/src/assets/svelte.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/src/main.ts: -------------------------------------------------------------------------------- 1 | import './app.css' 2 | import App from './App.svelte' 3 | import About from './About.svelte' 4 | 5 | 6 | 7 | const app = new App({ 8 | target: document.getElementById('app'), 9 | }) 10 | 11 | export default app 12 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true 17 | }, 18 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /usage/svelte/svelte-project/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [svelte()], 7 | }) 8 | -------------------------------------------------------------------------------- /usage/vue/vue-project/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /usage/vue/vue-project/README.md: -------------------------------------------------------------------------------- 1 | # vue-project 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | -------------------------------------------------------------------------------- /usage/vue/vue-project/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /usage/vue/vue-project/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /usage/vue/vue-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-project", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "copy-cdn-file-for-test": "cp ./../../../package/dist/cdn/client-script.js ./public/cdn", 8 | "build": "run-p type-check build-only", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --noEmit" 12 | }, 13 | "dependencies": { 14 | "serverless-website-analytics-client": "^1.3.1", 15 | "vue": "^3.2.45", 16 | "vue-router": "^4.1.6" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^18.11.12", 20 | "@vitejs/plugin-vue": "^4.0.0", 21 | "@vue/tsconfig": "^0.1.3", 22 | "npm-run-all": "^4.1.5", 23 | "typescript": "~4.7.4", 24 | "vite": "^4.0.0", 25 | "vue-tsc": "^1.0.12" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /usage/vue/vue-project/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rehanvdm/serverless-website-analytics-client/5e233c215e51457edcefbf6eef54f7c8e83c6fc9/usage/vue/vue-project/public/favicon.ico -------------------------------------------------------------------------------- /usage/vue/vue-project/src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | 36 | 99 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 70 | font-size: 15px; 71 | text-rendering: optimizeLegibility; 72 | -webkit-font-smoothing: antialiased; 73 | -moz-osx-font-smoothing: grayscale; 74 | } 75 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 41 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/components/TheWelcome.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 87 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/components/icons/IconSupport.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | // import * as swaClient from '../../../../package/dist/index'; 5 | import * as swaClient from '../../../../package/src'; 6 | // import * as swaClient from 'serverless-website-analytics-client'; 7 | 8 | import './assets/main.css' 9 | 10 | const app = createApp(App); 11 | app.use(router); 12 | 13 | swaClient.v1.analyticsPageInit({ 14 | inBrowser: true, 15 | site: "tests", 16 | apiUrl: "http://localhost:3000", 17 | // apiUrl: "https://d3nhr87nci4rd5.cloudfront.net", 18 | // debug: true, 19 | userId: "test-user-id" 20 | }); 21 | router.afterEach((event) => { 22 | swaClient.v1.analyticsPageChange(event.path); 23 | }); 24 | 25 | app.mount('#app'); 26 | 27 | export { swaClient }; 28 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import HomeView from '../views/HomeView.vue' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'home', 10 | component: HomeView 11 | }, 12 | { 13 | path: '/about', 14 | name: 'about', 15 | // route level code-splitting 16 | // this generates a separate chunk (About.[hash].js) for this route 17 | // which is lazy-loaded when the route is visited. 18 | component: () => import('../views/AboutView.vue') 19 | } 20 | ] 21 | }) 22 | 23 | export default router 24 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /usage/vue/vue-project/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /usage/vue/vue-project/tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /usage/vue/vue-project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | }, 10 | 11 | "references": [ 12 | { 13 | "path": "./tsconfig.config.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /usage/vue/vue-project/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | '@': fileURLToPath(new URL('./src', import.meta.url)) 12 | } 13 | } 14 | }) 15 | --------------------------------------------------------------------------------