debugbar 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.prettierrc +6 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +5 -0
  5. data/Rakefile +4 -0
  6. data/app/channels/debugbar/debugbar_channel.rb +26 -0
  7. data/app/controllers/debugbar/application_controller.rb +4 -0
  8. data/app/controllers/debugbar/assets_controller.rb +15 -0
  9. data/app/helpers/debugbar/tag_helpers.rb +19 -0
  10. data/app/models/debugbar/application_record.rb +5 -0
  11. data/client/.gitignore +25 -0
  12. data/client/README.md +0 -0
  13. data/client/dist/.vite/manifest.json +14 -0
  14. data/client/dist/assets/debugbar-u5mP-5-z.js +34 -0
  15. data/client/dist/assets/ruby-logo-kn_8RniZ.svg +946 -0
  16. data/client/index.html +78 -0
  17. data/client/package-lock.json +2393 -0
  18. data/client/package.json +32 -0
  19. data/client/postcss.config.js +6 -0
  20. data/client/src/App.vue +17 -0
  21. data/client/src/AppDemo.vue +29 -0
  22. data/client/src/AppDev.vue +20 -0
  23. data/client/src/Debugbar.vue +276 -0
  24. data/client/src/assets/rails-logo.svg +1 -0
  25. data/client/src/assets/ruby-logo.svg +946 -0
  26. data/client/src/components/TabButton.vue +32 -0
  27. data/client/src/components/panels/CachePanel.vue +41 -0
  28. data/client/src/components/panels/JobsPanel.vue +52 -0
  29. data/client/src/components/panels/JsonPanel.vue +15 -0
  30. data/client/src/components/panels/LogsPanel.vue +43 -0
  31. data/client/src/components/panels/MessagesPanel.vue +25 -0
  32. data/client/src/components/panels/ModelsPanel.vue +37 -0
  33. data/client/src/components/panels/Panel.vue +7 -0
  34. data/client/src/components/panels/RequestPanel.vue +98 -0
  35. data/client/src/components/queries/QueriesPanel.vue +17 -0
  36. data/client/src/components/queries/QueryItem.vue +65 -0
  37. data/client/src/components/ui/Foldable.vue +39 -0
  38. data/client/src/components/ui/KeyValueTable.vue +16 -0
  39. data/client/src/components/ui/Row.vue +14 -0
  40. data/client/src/components/ui/logo-ruby.vue +547 -0
  41. data/client/src/demo.ts +17 -0
  42. data/client/src/dev.ts +20 -0
  43. data/client/src/main.ts +17 -0
  44. data/client/src/models/Config.ts +27 -0
  45. data/client/src/models/Request.ts +183 -0
  46. data/client/src/stores/RequestsStore.ts +36 -0
  47. data/client/src/stores/configStore.ts +8 -0
  48. data/client/src/style.css +23 -0
  49. data/client/src/types.d.ts +9 -0
  50. data/client/src/vite-env.d.ts +1 -0
  51. data/client/tailwind.config.js +16 -0
  52. data/client/tsconfig.json +29 -0
  53. data/client/tsconfig.node.json +10 -0
  54. data/client/vite.config.ts +44 -0
  55. data/config/routes.rb +7 -0
  56. data/debugbar.gemspec +38 -0
  57. data/fixtures/requests/1706607114--demo_post_list.json +499 -0
  58. data/fixtures/requests/1706607120--api_jobs.json +176 -0
  59. data/fixtures/requests/1706607123--api_jobs.json +119 -0
  60. data/fixtures/requests/1706607133--demo_slow_page.json +130 -0
  61. data/fixtures/requests/1706607136--demo_post.json +164 -0
  62. data/fixtures/requests/1706607136--demo_random_post.json +106 -0
  63. data/fixtures/requests/1706607141--api_errors.json +73 -0
  64. data/lib/debugbar/buffers/memory_buffer.rb +34 -0
  65. data/lib/debugbar/buffers/null_buffer.rb +18 -0
  66. data/lib/debugbar/buffers/request_buffer.rb +31 -0
  67. data/lib/debugbar/config.rb +50 -0
  68. data/lib/debugbar/current.rb +18 -0
  69. data/lib/debugbar/engine.rb +96 -5
  70. data/lib/debugbar/loggers/simple_logger.rb +34 -0
  71. data/lib/debugbar/middlewares/track_current_request.rb +35 -0
  72. data/lib/debugbar/request.rb +98 -0
  73. data/lib/debugbar/subscribers/action_controller.rb +33 -0
  74. data/lib/debugbar/subscribers/active_job.rb +158 -0
  75. data/lib/debugbar/subscribers/active_record.rb +65 -0
  76. data/lib/debugbar/subscribers/active_support.rb +30 -0
  77. data/lib/debugbar/version.rb +5 -0
  78. data/lib/debugbar.rb +86 -5
  79. data/package-lock.json +50 -0
  80. data/package.json +5 -0
  81. data/sig/debugbar.rbs +4 -0
  82. metadata +143 -47
  83. data/README.textile +0 -18
  84. data/lib/debugbar/railties/tasks.rake +0 -1
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "client",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vue-tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@heroicons/vue": "^2.1.1",
13
+ "@highlightjs/vue-plugin": "^2.1.0",
14
+ "@rails/actioncable": "^6.1.7",
15
+ "lodash": "^4.17.21",
16
+ "pinia": "^2.1.7",
17
+ "sql-formatter": "^15.0.2",
18
+ "vue": "^3.3.8",
19
+ "vue-shadow-dom": "^4.2.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/lodash": "^4.14.202",
23
+ "@types/node": "^20.10.4",
24
+ "@vitejs/plugin-vue": "^4.5.0",
25
+ "autoprefixer": "^10.4.16",
26
+ "postcss": "^8.4.31",
27
+ "tailwindcss": "^3.4",
28
+ "typescript": "^5.2.2",
29
+ "vite": "^5.0.0",
30
+ "vue-tsc": "^1.8.22"
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import Debugbar from "@/Debugbar.vue"
3
+ import { ShadowRoot, ShadowStyle } from "vue-shadow-dom"
4
+
5
+ import css from "./style.css?inline"
6
+ </script>
7
+
8
+ <template>
9
+ <div>
10
+ <shadow-root id="__debugbar-shadow-root">
11
+ <debugbar></debugbar>
12
+ <shadow-style>
13
+ {{ css }}
14
+ </shadow-style>
15
+ </shadow-root>
16
+ </div>
17
+ </template>
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ import Debugbar from "@/Debugbar.vue"
3
+ import { ShadowRoot, ShadowStyle } from "vue-shadow-dom"
4
+ import { onMounted } from "vue"
5
+ import { useRequestsStore } from "@/stores/RequestsStore.ts"
6
+ import { BackendRequestData } from "@/models/Request.ts"
7
+
8
+ import css from "./style.css?inline"
9
+
10
+ onMounted(() => {
11
+ console.log(`Using debugbar in DEMO mode`)
12
+
13
+ const requests = import.meta.glob("../../fixtures/requests/*.json", { eager: true })
14
+ const firstId = useRequestsStore().addRequests(Object.values(requests) as BackendRequestData[])[0]
15
+
16
+ useRequestsStore().setCurrentRequestById(firstId)
17
+ })
18
+ </script>
19
+
20
+ <template>
21
+ <div>
22
+ <shadow-root id="__debugbar-shadow-root">
23
+ <debugbar></debugbar>
24
+ <shadow-style>
25
+ {{ css }}
26
+ </shadow-style>
27
+ </shadow-root>
28
+ </div>
29
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import Debugbar from "@/Debugbar.vue"
3
+ import { onMounted } from "vue"
4
+ import { useRequestsStore } from "@/stores/RequestsStore.ts"
5
+ import { BackendRequestData } from "@/models/Request.ts"
6
+
7
+ onMounted(() => {
8
+ if (import.meta.env.DEV && import.meta.env.VITE_LOAD_FIXTURES) {
9
+ const requests = import.meta.glob("../../fixtures/requests/*.json", { eager: true })
10
+ const lastId = useRequestsStore()
11
+ .addRequests(Object.values(requests) as BackendRequestData[])
12
+ .pop()
13
+ useRequestsStore().setCurrentRequestById(lastId)
14
+ }
15
+ })
16
+ </script>
17
+
18
+ <template>
19
+ <debugbar />
20
+ </template>
@@ -0,0 +1,276 @@
1
+ <script setup lang="ts">
2
+ import { createConsumer } from "@rails/actioncable"
3
+ import { computed, onMounted, reactive, ref } from "vue"
4
+ import { CodeBracketIcon, XCircleIcon, ArrowDownLeftIcon, TrashIcon } from "@heroicons/vue/16/solid"
5
+
6
+ import TabButton from "@/components/TabButton.vue"
7
+ import ModelsPanel from "@/components/panels/ModelsPanel.vue"
8
+ import QueriesPanel from "@/components/queries/QueriesPanel.vue"
9
+ import JobsPanel from "@/components/panels/JobsPanel.vue"
10
+ import LogsPanel from "@/components/panels/LogsPanel.vue"
11
+ import MessagesPanel from "@/components/panels/MessagesPanel.vue"
12
+
13
+ import { useRequestsStore } from "@/stores/RequestsStore.ts"
14
+ import { useConfigStore } from "@/stores/configStore.ts"
15
+ import CachePanel from "@/components/panels/CachePanel.vue"
16
+ import RequestPanel from "@/components/panels/RequestPanel.vue"
17
+ import JsonPanel from "@/components/panels/JsonPanel.vue"
18
+ import LogoRuby from "@/components/ui/logo-ruby.vue"
19
+
20
+ let requestsStore = useRequestsStore()
21
+ let configStore = useConfigStore()
22
+
23
+ const header = ref(null)
24
+
25
+ const state = reactive({
26
+ activeTab: "",
27
+ minimized: false,
28
+ isResizing: false,
29
+ height: configStore.config.height,
30
+ })
31
+
32
+ const isActive = computed(() => {
33
+ return state.activeTab != ""
34
+ })
35
+
36
+ const devMode = computed(() => {
37
+ return import.meta.env.DEV
38
+ })
39
+
40
+ const routeAlias = computed(() => {
41
+ return requestsStore.currentRequest?.meta.params.controller + "#" + requestsStore.currentRequest?.meta.params.action
42
+ })
43
+
44
+ let debugbarChannel
45
+
46
+ if (configStore.config.mode == "ws") {
47
+ debugbarChannel = createConsumer(configStore.config.actionCableUrl).subscriptions.create(
48
+ { channel: configStore.config.channelName },
49
+ {
50
+ connected() {
51
+ console.log("🟢 Connected to channel")
52
+ debugbarChannel.send({ ids: [] })
53
+ },
54
+
55
+ disconnected() {
56
+ console.log("🔴 Disconnected from channel")
57
+ },
58
+ received(data) {
59
+ if (data.length == 0) {
60
+ return
61
+ }
62
+
63
+ const ids = requestsStore.addRequests(data)
64
+
65
+ if (!isActive.value) {
66
+ requestsStore.setCurrentRequestById(ids[ids.length - 1])
67
+ }
68
+
69
+ setTimeout(() => {
70
+ debugbarChannel.send({ ids: ids })
71
+ }, 50)
72
+ },
73
+ }
74
+ )
75
+ } else {
76
+ console.log(`Using debugbar in "offline mode", ideal for demoing with fixtures.`)
77
+ debugbarChannel = {
78
+ send: (data) => {
79
+ // No-op
80
+ console.log("Ignoring `send` call", data)
81
+ },
82
+ }
83
+ }
84
+
85
+ const clearRequests = () => {
86
+ console.log("Clearing requests")
87
+ state.activeTab = ""
88
+ requestsStore.clearRequests()
89
+ debugbarChannel.send({ clear: true })
90
+ }
91
+
92
+ // Resizing the debugbar
93
+ onMounted(() => {
94
+ window.onresize = function () {
95
+ if (window.innerHeight < state.height) {
96
+ state.height = window.innerHeight - header.value.clientHeight
97
+ }
98
+ }
99
+
100
+ document.onmousemove = function (e) {
101
+ if (!state.isResizing) {
102
+ return
103
+ }
104
+
105
+ state.height = window.innerHeight - e.clientY - header.value.clientHeight
106
+ }
107
+
108
+ document.onmouseup = function (_e) {
109
+ state.isResizing = false
110
+ }
111
+ })
112
+
113
+ const setActiveTab = (tab) => {
114
+ if (state.activeTab == tab) {
115
+ state.activeTab = "" // Close if you click on active tab
116
+ } else {
117
+ if (window.innerHeight < state.height) {
118
+ state.height = window.innerHeight - header.value.clientHeight * 2
119
+ }
120
+ state.activeTab = tab
121
+ }
122
+ }
123
+ </script>
124
+
125
+ <template>
126
+ <div
127
+ v-if="state.minimized"
128
+ @click="state.minimized = false"
129
+ class="z-[9999] fixed left-0 bottom-0 bg-transparent cursor-pointer"
130
+ >
131
+ <div class="p-1 pt-1.5">
132
+ <img class="h-5" src="./assets/ruby-logo.svg" alt="Logo" />
133
+ </div>
134
+ </div>
135
+
136
+ <div v-if="!state.minimized && requestsStore.currentRequest == null" class="z-[9999] fixed left-0 bottom-0 w-full">
137
+ <div class="h-0.5 bg-red-700 cursor-row-resize" />
138
+ <div class="flex items-center justify-between bg-stone-100 border-b border-stone-200">
139
+ <div class="px-5 py-1.5 italic">No request yet</div>
140
+ <div class="px-3">
141
+ <button @click="state.minimized = true" title="Hide in the corner">
142
+ <arrow-down-left-icon class="size-4" />
143
+ </button>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ <div v-if="!state.minimized && requestsStore.currentRequest" class="z-[9999] fixed left-0 bottom-0 w-full">
149
+ <div id="draggable-bar" @mousedown="state.isResizing = true" class="h-0.5 bg-red-700 cursor-row-resize" />
150
+
151
+ <div
152
+ id="debubgbar-header"
153
+ ref="header"
154
+ class="flex px-1 items-center justify-between bg-stone-100 border-b-2 border-stone-300"
155
+ >
156
+ <!-- Left -->
157
+ <div>
158
+ <div class="flex">
159
+ <div class="p-1 pt-1.5">
160
+ <logo-ruby />
161
+ </div>
162
+
163
+ <tab-button
164
+ v-for="(v, k) in requestsStore.currentRequest.dataForTabs"
165
+ key="k"
166
+ :label="v.label"
167
+ :count="v?.count"
168
+ :is-active="k === state.activeTab"
169
+ :disabled="v.count == 0"
170
+ @click="setActiveTab(k)"
171
+ >{{ v.label }}</tab-button
172
+ >
173
+
174
+ <button
175
+ v-if="devMode"
176
+ @click="setActiveTab('debug')"
177
+ class="px-3 py-1.5 text-stone-600"
178
+ :class="{ 'bg-stone-300': state.activeTab == 'debug' }"
179
+ >
180
+ <CodeBracketIcon class="size-4" />
181
+ </button>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- Right -->
186
+ <div class="flex items-center space-x-2.5 pr-1">
187
+ <div @click="setActiveTab('request')" class="flex space-x-2 cursor-pointer">
188
+ <span class="text-sm text-stone-600 font-medium tracking-wide">
189
+ {{ routeAlias }}
190
+ </span>
191
+
192
+ <span
193
+ class="text-sm font-bold"
194
+ v-if="requestsStore.currentRequest.meta.duration < 1000"
195
+ :class="{
196
+ 'text-orange-600': requestsStore.currentRequest.meta.duration >= 800,
197
+ }"
198
+ >{{ requestsStore.currentRequest.meta.duration.toFixed(1) }}ms</span
199
+ >
200
+ <span
201
+ class="text-sm font-bold text-red-600 bg-red-100 px-1 rounded"
202
+ v-if="requestsStore.currentRequest.meta.duration >= 1000"
203
+ >{{ (requestsStore.currentRequest.meta.duration / 1000).toFixed(2) }}s</span
204
+ >
205
+
206
+ <span
207
+ class="px-1 py-0.5 rounded text-xs"
208
+ :class="{
209
+ 'bg-green-600 text-white': requestsStore.currentRequest.meta.status < 300,
210
+ 'bg-amber-600 text-white':
211
+ requestsStore.currentRequest.meta.status >= 400 && requestsStore.currentRequest.meta.status < 500,
212
+ 'bg-red-600 text-white': requestsStore.currentRequest.meta.status >= 500,
213
+ }"
214
+ >{{ requestsStore.currentRequest.meta.status }}</span
215
+ >
216
+ </div>
217
+
218
+ <select
219
+ class="px-2 py-1.5 bg-white border border-stone-200 rounded w-[330px] text-sm"
220
+ name="current_request_id"
221
+ @change="
222
+ (event) => {
223
+ const target = event.target as HTMLSelectElement
224
+ requestsStore.setCurrentRequestById(target.value)
225
+ }
226
+ "
227
+ >
228
+ <option
229
+ v-for="r in requestsStore.requests"
230
+ :selected="requestsStore.currentRequest.id == r.id"
231
+ v-text="r.pathWithVerb"
232
+ :value="r.id"
233
+ />
234
+ </select>
235
+
236
+ <button @click="clearRequests" title="Clear all requests (frontend and backend)">
237
+ <trash-icon class="size-4" />
238
+ </button>
239
+
240
+ <div class="flex items-center pl-2 space-x-2">
241
+ <button @click="state.minimized = true" title="Hide in the corner">
242
+ <arrow-down-left-icon class="size-4" />
243
+ </button>
244
+ <button :disabled="!isActive" @click="state.activeTab = ''" title="Close">
245
+ <x-circle-icon class="size-4" />
246
+ </button>
247
+ </div>
248
+ </div>
249
+ </div>
250
+
251
+ <div
252
+ ref="body"
253
+ id="debugbar-body"
254
+ class="bg-white overflow-scroll"
255
+ v-if="state.activeTab != ''"
256
+ :style="`height: ${state.height}px`"
257
+ >
258
+ <request-panel v-if="state.activeTab == 'request'" :request="requestsStore.currentRequest" />
259
+ <messages-panel v-if="state.activeTab == 'messages'" :messages="requestsStore.currentRequest?.messages" />
260
+ <models-panel
261
+ v-if="state.activeTab == 'models'"
262
+ :models="requestsStore.currentRequest?.models"
263
+ :count="requestsStore.currentRequest?.modelsCount"
264
+ />
265
+ <queries-panel v-if="state.activeTab == 'queries'" :current-request="requestsStore.currentRequest" />
266
+ <jobs-panel v-if="state.activeTab == 'jobs'" :jobs="requestsStore.currentRequest?.jobs" />
267
+ <cache-panel v-if="state.activeTab == 'cache'" :cache="requestsStore.currentRequest?.cache" />
268
+ <logs-panel v-if="state.activeTab == 'logs'" :logs="requestsStore.currentRequest?.logs" />
269
+ <json-panel
270
+ v-if="devMode && state.activeTab == 'debug'"
271
+ :current-request="requestsStore.currentRequest"
272
+ class="px-3 py-2"
273
+ />
274
+ </div>
275
+ </div>
276
+ </template>
@@ -0,0 +1 @@
1
+ <svg height="32" viewBox="0 0 90 32" width="90" xmlns="http://www.w3.org/2000/svg"><path d="m418.082357 25.9995403v4.1135034h-7.300339v1.89854h3.684072c1.972509 0 4.072534 1.4664311 4.197997 3.9665124l.005913.2373977v1.5821167c-.087824 3.007959-2.543121 4.1390018-4.071539 4.2011773l-.132371.0027328h-7.390745v-4.0909018l7.481152-.0226016v-1.9889467l-1.190107.0007441-.346911.0008254-.084566.0003251-.127643.0007097-.044785.0003793-.055764.0007949-.016378.0008259c.000518.0004173.013246.0008384.034343.0012518l.052212.000813c.030547.0003979.066903.0007803.105225.0011355l.078131.0006709-.155385-.0004701c-.31438-.001557-.85249-.0041098-1.729029-.0080055-1.775258 0-4.081832-1.3389153-4.219994-3.9549201l-.006518-.24899v-1.423905c0-2.6982402 2.278213-4.182853 4.065464-4.2678491l.161048-.003866zm-18.691579 0v11.8658752h6.170255v4.1361051h-10.735792v-16.0019803zm-6.441475 0v16.0019803h-4.588139v-16.0019803zm-10.803597 0c1.057758 0 4.04923.7305141 4.198142 3.951222l.005768.2526881v11.7980702h-4.271715v-2.8252084h-4.136105v2.8252084h-4.407325v-11.7980702c0-1.3184306 1.004082-4.0468495 3.946899-4.197411l.257011-.0064991zm-24.147177-.0027581 8.580186.0005749c.179372.0196801 4.753355.5702841 4.753355 5.5438436s-3.775694 5.3947112-3.92376 5.4093147l-.004472.0004216 5.00569 5.0505836h-6.374959l-3.726209-3.8608906v3.8608906h-4.309831zm22.418634-2.6971669.033418.0329283s-.384228.27122-.791058.610245c-12.837747-9.4927002-20.680526-5.0175701-23.144107-3.8196818-11.187826 6.2428065-7.954768 21.5678895-7.888988 21.8737669l.001006.0046469h-17.855317s.67805-6.6900935 5.4244-14.600677c4.74635-7.9105834 12.837747-13.9000252 19.414832-14.4876686 12.681632-1.2703535 24.110975 9.7062594 24.805814 10.3864403zm-31.111679 14.1815719 2.44098.881465c.113008.8852319.273103 1.7233771.441046 2.4882761l.101394.4499406-2.7122-.9718717c-.113009-.67805-.226017-1.6499217-.27122-2.84781zm31.506724-7.6619652h-1.514312c-1.128029 0-1.333125.5900716-1.370415.8046431l-.007251.056292-.000906.0152319-.00013 3.9153864h4.136105l-.000316-3.916479c-.004939-.0795522-.08331-.8750744-1.242775-.8750744zm-50.492125.339025 2.599192.94927c-.316423.731729-.719369 1.6711108-1.011998 2.4093289l-.118085.3028712-2.599192-.94927c.226017-.610245.700652-1.7403284 1.130083-2.7122001zm35.445121-.1434449h-3.456844v3.6588673h3.434397s.98767-.3815997.98767-1.8406572-.965223-1.8182101-.965223-1.8182101zm-15.442645-.7606218 1.62732 1.2882951c-.180814.705172-.318232 1.410344-.412255 2.115516l-.06238.528879-1.830735-1.4465067c.180813-.81366.384228-1.6499217.67805-2.4861834zm4.000495-6.3058651 1.017075 1.5369134c-.39779.4158707-.766649.8317413-1.095006 1.2707561l-.238493.3339623-1.08488-1.6273201c.40683-.5198383.881465-1.0396767 1.401304-1.5143117zm-16.182794-3.3450467 1.604719 1.4013034c-.40683.4237812-.800947.8729894-1.172815 1.3285542l-.364099.4569775-1.740328-1.4917101c.519838-.5650416 1.08488-1.1300833 1.672523-1.695125zm22.398252-.0904067.497237 1.4917101c-.524359.162732-1.048717.3688592-1.573076.6068095l-.393269.1842488-.519838-1.559515c.565041-.2486184 1.22049-.4972367 1.988946-.7232534zm5.28879-.54244c.578603.0361627 1.171671.1012555 1.779204.2068505l.458361.0869712-.090406 1.4013034c-.596684-.1265694-1.193368-.2097435-1.790052-.2495224l-.447513-.0216976zm-18.555968-6.2380601 1.017075 1.559515c-.440733.2203663-.868752.4661594-1.303128.7278443l-.437201.2666291-1.039676-1.5821167c.610245-.3616267 1.197888-.67805 1.76293-.9718717zm18.601172-.8588633c1.344799.3842283 1.923513.6474959 2.155025.7707625l.037336.0202958-.090406 1.5143117c-.482169-.1958811-.964338-.381717-1.453204-.5575078l-.739158-.2561522zm-8.633837-1.3334984.452033 1.3787017h-.226016c-.491587 0-.983173.0127134-1.474759.0476754l-.491587.0427313-.429431-1.3334984c.745855-.0904067 1.469108-.13561 2.16976-.13561z" transform="translate(-329 -10)"/></svg>