logster 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +19 -19
  3. data/.rubocop.yml +1 -1
  4. data/.travis.yml +16 -16
  5. data/CHANGELOG.md +172 -169
  6. data/Gemfile +4 -4
  7. data/Guardfile +8 -8
  8. data/LICENSE.txt +22 -22
  9. data/README.md +99 -99
  10. data/Rakefile +21 -21
  11. data/assets/fonts/FontAwesome.otf +0 -0
  12. data/assets/fonts/fontawesome-webfont.eot +0 -0
  13. data/assets/fonts/fontawesome-webfont.svg +639 -639
  14. data/assets/fonts/fontawesome-webfont.ttf +0 -0
  15. data/assets/fonts/fontawesome-webfont.woff +0 -0
  16. data/assets/fonts/fontawesome-webfont.woff2 +0 -0
  17. data/assets/images/Icon-144_rounded.png +0 -0
  18. data/assets/images/Icon-144_square.png +0 -0
  19. data/assets/images/icon_144x144.png +0 -0
  20. data/assets/images/icon_64x64.png +0 -0
  21. data/assets/javascript/client-app.js +106 -100
  22. data/assets/stylesheets/client-app.css +1 -1
  23. data/build_client_app.sh +0 -0
  24. data/client-app/.editorconfig +20 -20
  25. data/client-app/.ember-cli +9 -9
  26. data/client-app/.eslintignore +19 -19
  27. data/client-app/.eslintrc.js +46 -46
  28. data/client-app/.gitignore +23 -23
  29. data/client-app/.travis.yml +27 -27
  30. data/client-app/.watchmanconfig +3 -3
  31. data/client-app/README.md +57 -57
  32. data/client-app/app/app.js +0 -0
  33. data/client-app/app/components/actions-menu.js +43 -37
  34. data/client-app/app/components/env-tab.js +80 -44
  35. data/client-app/app/components/message-info.js +0 -0
  36. data/client-app/app/components/message-row.js +0 -0
  37. data/client-app/app/components/panel-resizer.js +0 -0
  38. data/client-app/app/components/tab-contents.js +27 -27
  39. data/client-app/app/components/tabbed-section.js +0 -0
  40. data/client-app/app/components/time-formatter.js +0 -0
  41. data/client-app/app/components/update-time.js +0 -0
  42. data/client-app/app/controllers/index.js +0 -0
  43. data/client-app/app/controllers/show.js +0 -0
  44. data/client-app/app/index.html +29 -29
  45. data/client-app/app/initializers/app-init.js +67 -72
  46. data/client-app/app/lib/preload.js +20 -14
  47. data/client-app/app/lib/utilities.js +149 -140
  48. data/client-app/app/models/message-collection.js +0 -0
  49. data/client-app/app/models/message.js +100 -100
  50. data/client-app/app/resolver.js +0 -0
  51. data/client-app/app/router.js +0 -0
  52. data/client-app/app/routes/index.js +0 -0
  53. data/client-app/app/routes/show.js +0 -0
  54. data/client-app/app/styles/app.css +527 -521
  55. data/client-app/app/templates/application.hbs +2 -2
  56. data/client-app/app/templates/components/actions-menu.hbs +12 -12
  57. data/client-app/app/templates/components/env-tab.hbs +10 -10
  58. data/client-app/app/templates/components/message-info.hbs +41 -41
  59. data/client-app/app/templates/components/message-row.hbs +15 -15
  60. data/client-app/app/templates/components/panel-resizer.hbs +3 -3
  61. data/client-app/app/templates/components/tabbed-section.hbs +10 -10
  62. data/client-app/app/templates/components/time-formatter.hbs +1 -1
  63. data/client-app/app/templates/index.hbs +58 -58
  64. data/client-app/app/templates/show.hbs +7 -7
  65. data/client-app/config/environment.js +51 -51
  66. data/client-app/config/optional-features.json +3 -3
  67. data/client-app/config/targets.js +18 -18
  68. data/client-app/ember-cli-build.js +29 -29
  69. data/client-app/package-lock.json +11365 -11365
  70. data/client-app/package.json +56 -56
  71. data/client-app/testem.js +25 -25
  72. data/client-app/tests/index.html +34 -34
  73. data/client-app/tests/integration/components/env-tab-test.js +123 -73
  74. data/client-app/tests/integration/components/message-info-test.js +111 -26
  75. data/client-app/tests/test-helper.js +8 -8
  76. data/client-app/tests/unit/controllers/index-test.js +12 -12
  77. data/client-app/tests/unit/controllers/show-test.js +12 -12
  78. data/client-app/tests/unit/initializers/app-init-test.js +31 -31
  79. data/client-app/tests/unit/routes/index-test.js +11 -11
  80. data/client-app/tests/unit/routes/show-test.js +11 -11
  81. data/lib/examples/sidekiq_logster_reporter.rb +21 -21
  82. data/lib/logster.rb +54 -54
  83. data/lib/logster/base_store.rb +141 -141
  84. data/lib/logster/configuration.rb +26 -25
  85. data/lib/logster/defer_logger.rb +14 -14
  86. data/lib/logster/ignore_pattern.rb +65 -65
  87. data/lib/logster/logger.rb +113 -113
  88. data/lib/logster/message.rb +212 -212
  89. data/lib/logster/middleware/debug_exceptions.rb +26 -26
  90. data/lib/logster/middleware/reporter.rb +55 -55
  91. data/lib/logster/middleware/viewer.rb +222 -221
  92. data/lib/logster/rails/railtie.rb +63 -63
  93. data/lib/logster/redis_store.rb +566 -566
  94. data/lib/logster/scheduler.rb +54 -54
  95. data/lib/logster/version.rb +3 -3
  96. data/lib/logster/web.rb +14 -14
  97. data/logster.gemspec +35 -35
  98. data/test/examples/test_sidekiq_reporter_example.rb +46 -46
  99. data/test/fake_data/Gemfile +4 -4
  100. data/test/fake_data/generate.rb +10 -10
  101. data/test/logster/middleware/test_reporter.rb +19 -19
  102. data/test/logster/middleware/test_viewer.rb +96 -96
  103. data/test/logster/test_base_store.rb +147 -147
  104. data/test/logster/test_defer_logger.rb +34 -34
  105. data/test/logster/test_ignore_pattern.rb +41 -41
  106. data/test/logster/test_logger.rb +86 -86
  107. data/test/logster/test_message.rb +119 -119
  108. data/test/logster/test_redis_rate_limiter.rb +230 -230
  109. data/test/logster/test_redis_store.rb +720 -720
  110. data/test/test_helper.rb +38 -38
  111. data/vendor/assets/javascripts/logster.js.erb +39 -39
  112. metadata +1 -10
  113. data/client-app/app/components/tab-link.js +0 -5
  114. data/client-app/tests/integration/components/actions-menu-test.js +0 -26
  115. data/client-app/tests/integration/components/message-row-test.js +0 -26
  116. data/client-app/tests/integration/components/panel-resizer-test.js +0 -26
  117. data/client-app/tests/integration/components/tab-contents-test.js +0 -26
  118. data/client-app/tests/integration/components/tab-link-test.js +0 -26
  119. data/client-app/tests/integration/components/tabbed-section-test.js +0 -26
  120. data/client-app/tests/integration/components/time-formatter-test.js +0 -26
  121. data/client-app/tests/integration/components/update-time-test.js +0 -26
File without changes
File without changes
File without changes
@@ -1,29 +1,29 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
- <title>ClientApp</title>
7
- <meta name="description" content="">
8
- <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=yes">
9
- <meta id="preloaded-data" data-root-path="/logs" data-preloaded="{}">
10
-
11
- {{content-for "head"}}
12
-
13
- <link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">
14
- <link integrity="" rel="stylesheet" href="{{rootURL}}assets/client-app.css">
15
-
16
- <link href='//fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'>
17
- <link href='//fonts.googleapis.com/css?family=Roboto+Mono' rel='stylesheet' type='text/css'>
18
-
19
- {{content-for "head-footer"}}
20
- </head>
21
- <body>
22
- {{content-for "body"}}
23
-
24
- <script src="{{rootURL}}assets/vendor.js"></script>
25
- <script src="{{rootURL}}assets/client-app.js"></script>
26
-
27
- {{content-for "body-footer"}}
28
- </body>
29
- </html>
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <title>ClientApp</title>
7
+ <meta name="description" content="">
8
+ <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=yes">
9
+ <meta id="preloaded-data" data-root-path="/logs" data-preloaded="{&quot;env_expandable_keys&quot;:[]}">
10
+
11
+ {{content-for "head"}}
12
+
13
+ <link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">
14
+ <link integrity="" rel="stylesheet" href="{{rootURL}}assets/client-app.css">
15
+
16
+ <link href='//fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'>
17
+ <link href='//fonts.googleapis.com/css?family=Roboto+Mono' rel='stylesheet' type='text/css'>
18
+
19
+ {{content-for "head-footer"}}
20
+ </head>
21
+ <body>
22
+ {{content-for "body"}}
23
+
24
+ <script src="{{rootURL}}assets/vendor.js"></script>
25
+ <script src="{{rootURL}}assets/client-app.js"></script>
26
+
27
+ {{content-for "body-footer"}}
28
+ </body>
29
+ </html>
@@ -1,72 +1,67 @@
1
- import {
2
- updateHiddenProperty,
3
- resetTitleCount
4
- } from "client-app/lib/utilities";
5
- import { init } from "client-app/lib/preload";
6
- import Evented from "@ember/object/evented";
7
- import EmberObject from "@ember/object";
8
-
9
- const TARGETS = ["component", "route"];
10
-
11
- export function initialize(app) {
12
- // config for moment.js
13
- moment.updateLocale("en", {
14
- relativeTime: {
15
- future: "in %s",
16
- past: "%s ago",
17
- s: "secs",
18
- m: "a min",
19
- mm: "%d mins",
20
- h: "an hr",
21
- hh: "%d hrs",
22
- d: "a day",
23
- dd: "%d days",
24
- M: "a mth",
25
- MM: "%d mths",
26
- y: "a yr",
27
- yy: "%d yrs"
28
- }
29
- });
30
-
31
- // parse preloaded json
32
- const dataset = document.getElementById("preloaded-data").dataset;
33
- init(dataset);
34
-
35
- // setup event for updating document title and title count
36
- let hiddenProperty;
37
- let visibilitychange;
38
-
39
- ["", "webkit", "ms", "moz", "ms"].forEach(prefix => {
40
- const check = prefix + (prefix === "" ? "hidden" : "Hidden");
41
- if (document[check] !== undefined && !hiddenProperty) {
42
- hiddenProperty = check;
43
- visibilitychange = prefix + "visibilitychange";
44
- }
45
- });
46
-
47
- updateHiddenProperty(hiddenProperty);
48
- document.addEventListener(
49
- visibilitychange,
50
- () => {
51
- resetTitleCount();
52
- },
53
- false
54
- );
55
-
56
- app.register("events:main", EmberObject.extend(Evented).create(), {
57
- instantiate: false
58
- });
59
- TARGETS.forEach(t => app.inject(t, "events", "events:main"));
60
-
61
- const isMobile =
62
- /mobile/i.test(navigator.userAgent) && !/iPad/.test(navigator.userAgent);
63
- if (isMobile) {
64
- Em.$("body").addClass("mobile");
65
- }
66
- app.register("site:main", { isMobile }, { instantiate: false });
67
- app.inject("controller", "site", "site:main");
68
- }
69
-
70
- export default {
71
- initialize
72
- };
1
+ import {
2
+ updateHiddenProperty,
3
+ resetTitleCount
4
+ } from "client-app/lib/utilities";
5
+ import Evented from "@ember/object/evented";
6
+ import EmberObject from "@ember/object";
7
+
8
+ const TARGETS = ["component", "route"];
9
+
10
+ export function initialize(app) {
11
+ // config for moment.js
12
+ moment.updateLocale("en", {
13
+ relativeTime: {
14
+ future: "in %s",
15
+ past: "%s ago",
16
+ s: "secs",
17
+ m: "a min",
18
+ mm: "%d mins",
19
+ h: "an hr",
20
+ hh: "%d hrs",
21
+ d: "a day",
22
+ dd: "%d days",
23
+ M: "a mth",
24
+ MM: "%d mths",
25
+ y: "a yr",
26
+ yy: "%d yrs"
27
+ }
28
+ });
29
+
30
+ // setup event for updating document title and title count
31
+ let hiddenProperty;
32
+ let visibilitychange;
33
+
34
+ ["", "webkit", "ms", "moz", "ms"].forEach(prefix => {
35
+ const check = prefix + (prefix === "" ? "hidden" : "Hidden");
36
+ if (document[check] !== undefined && !hiddenProperty) {
37
+ hiddenProperty = check;
38
+ visibilitychange = prefix + "visibilitychange";
39
+ }
40
+ });
41
+
42
+ updateHiddenProperty(hiddenProperty);
43
+ document.addEventListener(
44
+ visibilitychange,
45
+ () => {
46
+ resetTitleCount();
47
+ },
48
+ false
49
+ );
50
+
51
+ app.register("events:main", EmberObject.extend(Evented).create(), {
52
+ instantiate: false
53
+ });
54
+ TARGETS.forEach(t => app.inject(t, "events", "events:main"));
55
+
56
+ const isMobile =
57
+ /mobile/i.test(navigator.userAgent) && !/iPad/.test(navigator.userAgent);
58
+ if (isMobile) {
59
+ Em.$("body").addClass("mobile");
60
+ }
61
+ app.register("site:main", { isMobile }, { instantiate: false });
62
+ app.inject("controller", "site", "site:main");
63
+ }
64
+
65
+ export default {
66
+ initialize
67
+ };
@@ -1,14 +1,20 @@
1
- let CONTAINER;
2
-
3
- export function init(dataset) {
4
- CONTAINER = {
5
- rootPath: dataset.rootPath,
6
- preload: JSON.parse(dataset.preloaded)
7
- };
8
- }
9
-
10
- export default {
11
- get(key) {
12
- return Em.get(CONTAINER, key);
13
- }
14
- };
1
+ let CONTAINER;
2
+ let isInitialized = false;
3
+
4
+ // exported so that it can be used in tests
5
+ export function init() {
6
+ const dataset = document.getElementById("preloaded-data").dataset;
7
+ CONTAINER = Em.$.extend(JSON.parse(dataset.preloaded), {
8
+ rootPath: dataset.rootPath
9
+ });
10
+ isInitialized = true;
11
+ }
12
+
13
+ export default {
14
+ get(key) {
15
+ if (!isInitialized) {
16
+ init();
17
+ }
18
+ return Em.get(CONTAINER, key);
19
+ }
20
+ };
@@ -1,140 +1,149 @@
1
- import Preload from "client-app/lib/preload";
2
-
3
- const entityMap = {
4
- "&": "&amp;",
5
- "<": "&lt;",
6
- ">": "&gt;",
7
- '"': "&quot;",
8
- "'": "&#39;",
9
- "/": "&#x2F;"
10
- };
11
-
12
- export function escapeHtml(string) {
13
- return String(string).replace(/[&<>"'/]/g, s => entityMap[s]);
14
- }
15
-
16
- export function ajax(url, settings) {
17
- settings = settings || {};
18
- settings.headers = settings.headers || {};
19
- settings.headers["X-SILENCE-LOGGER"] = true;
20
- return Em.$.ajax(Preload.get("rootPath") + url, settings);
21
- }
22
-
23
- export function preloadOrAjax(url, settings) {
24
- const preloaded = Preload.get(`preload.${url.replace(".json", "")}`);
25
- if (preloaded) {
26
- return Em.RSVP.resolve(preloaded);
27
- } else {
28
- return ajax(url, settings);
29
- }
30
- }
31
-
32
- let HIDDEN_PROPERTY;
33
- let TITLE;
34
- let TITLE_COUNT;
35
-
36
- export function updateHiddenProperty(property) {
37
- HIDDEN_PROPERTY = property;
38
- }
39
-
40
- export function isHidden() {
41
- if (HIDDEN_PROPERTY !== undefined) {
42
- return document[HIDDEN_PROPERTY];
43
- } else {
44
- return !document.hasFocus;
45
- }
46
- }
47
-
48
- export function increaseTitleCount(increment) {
49
- if (!isHidden()) {
50
- return;
51
- }
52
- TITLE = TITLE || document.title;
53
- TITLE_COUNT = TITLE_COUNT || 0;
54
- TITLE_COUNT += increment;
55
- document.title = `${TITLE} (${TITLE_COUNT})`;
56
- }
57
-
58
- export function resetTitleCount() {
59
- TITLE_COUNT = 0;
60
- document.title = TITLE || document.title;
61
- }
62
-
63
- export function formatTime(timestamp) {
64
- let formatted;
65
- const time = moment(timestamp);
66
- const now = moment();
67
-
68
- if (time.diff(now.startOf("day")) > 0) {
69
- formatted = time.format("h:mm a");
70
- } else {
71
- if (time.diff(now.startOf("week")) > 0) {
72
- formatted = time.format("dd h:mm a");
73
- } else {
74
- if (time.diff(now.startOf("year")) > 0) {
75
- formatted = time.format("D MMM h:mm a");
76
- } else {
77
- formatted = time.format("D MMM YY");
78
- }
79
- }
80
- }
81
-
82
- return formatted;
83
- }
84
-
85
- export function buildArrayString(array) {
86
- const buffer = [];
87
- array.forEach(v => {
88
- if (v === null) {
89
- buffer.push("null");
90
- } else if (Object.prototype.toString.call(v) === "[object Array]") {
91
- buffer.push(buildArrayString(v));
92
- } else {
93
- buffer.push(escapeHtml(v.toString()));
94
- }
95
- });
96
- return "[" + buffer.join(", ") + "]";
97
- }
98
-
99
- export function buildHashString(hash, recurse) {
100
- if (!hash) return "";
101
-
102
- const buffer = [];
103
- const hashes = [];
104
- _.each(hash, (v, k) => {
105
- if (v === null) {
106
- buffer.push("null");
107
- } else if (Object.prototype.toString.call(v) === "[object Array]") {
108
- buffer.push(
109
- "<tr><td>" +
110
- escapeHtml(k) +
111
- "</td><td>" +
112
- buildArrayString(v) +
113
- "</td></tr>"
114
- );
115
- } else if (typeof v === "object") {
116
- hashes.push(k);
117
- } else {
118
- buffer.push(
119
- "<tr><td>" + escapeHtml(k) + "</td><td>" + escapeHtml(v) + "</td></tr>"
120
- );
121
- }
122
- });
123
-
124
- if (_.size(hashes) > 0) {
125
- _.each(hashes, function(k1) {
126
- const v = hash[k1];
127
- buffer.push("<tr><td></td><td><table>");
128
- buffer.push(
129
- "<td>" +
130
- escapeHtml(k1) +
131
- "</td><td>" +
132
- buildHashString(v, true) +
133
- "</td>"
134
- );
135
- buffer.push("</table></td></tr>");
136
- });
137
- }
138
- const className = recurse ? "" : "env-table";
139
- return "<table class='" + className + "'>" + buffer.join("\n") + "</table>";
140
- }
1
+ import Preload from "client-app/lib/preload";
2
+
3
+ const entityMap = {
4
+ "&": "&amp;",
5
+ "<": "&lt;",
6
+ ">": "&gt;",
7
+ '"': "&quot;",
8
+ "'": "&#39;",
9
+ "/": "&#x2F;"
10
+ };
11
+
12
+ export function escapeHtml(string) {
13
+ return String(string).replace(/[&<>"'/]/g, s => entityMap[s]);
14
+ }
15
+
16
+ export function ajax(url, settings) {
17
+ settings = settings || {};
18
+ settings.headers = settings.headers || {};
19
+ settings.headers["X-SILENCE-LOGGER"] = true;
20
+ return Em.$.ajax(Preload.get("rootPath") + url, settings);
21
+ }
22
+
23
+ export function preloadOrAjax(url, settings) {
24
+ const preloaded = Preload.get(url.replace(".json", ""));
25
+ if (preloaded) {
26
+ return Em.RSVP.resolve(preloaded);
27
+ } else {
28
+ return ajax(url, settings);
29
+ }
30
+ }
31
+
32
+ let HIDDEN_PROPERTY;
33
+ let TITLE;
34
+ let TITLE_COUNT;
35
+
36
+ export function updateHiddenProperty(property) {
37
+ HIDDEN_PROPERTY = property;
38
+ }
39
+
40
+ export function isHidden() {
41
+ if (HIDDEN_PROPERTY !== undefined) {
42
+ return document[HIDDEN_PROPERTY];
43
+ } else {
44
+ return !document.hasFocus;
45
+ }
46
+ }
47
+
48
+ export function increaseTitleCount(increment) {
49
+ if (!isHidden()) {
50
+ return;
51
+ }
52
+ TITLE = TITLE || document.title;
53
+ TITLE_COUNT = TITLE_COUNT || 0;
54
+ TITLE_COUNT += increment;
55
+ document.title = `${TITLE} (${TITLE_COUNT})`;
56
+ }
57
+
58
+ export function resetTitleCount() {
59
+ TITLE_COUNT = 0;
60
+ document.title = TITLE || document.title;
61
+ }
62
+
63
+ export function formatTime(timestamp) {
64
+ let formatted;
65
+ const time = moment(timestamp);
66
+ const now = moment();
67
+
68
+ if (time.diff(now.startOf("day")) > 0) {
69
+ formatted = time.format("h:mm a");
70
+ } else {
71
+ if (time.diff(now.startOf("week")) > 0) {
72
+ formatted = time.format("dd h:mm a");
73
+ } else {
74
+ if (time.diff(now.startOf("year")) > 0) {
75
+ formatted = time.format("D MMM h:mm a");
76
+ } else {
77
+ formatted = time.format("D MMM YY");
78
+ }
79
+ }
80
+ }
81
+
82
+ return formatted;
83
+ }
84
+
85
+ export function buildArrayString(array) {
86
+ const buffer = [];
87
+ array.forEach(v => {
88
+ if (v === null) {
89
+ buffer.push("null");
90
+ } else if (Object.prototype.toString.call(v) === "[object Array]") {
91
+ buffer.push(buildArrayString(v));
92
+ } else {
93
+ buffer.push(escapeHtml(v.toString()));
94
+ }
95
+ });
96
+ return "[" + buffer.join(", ") + "]";
97
+ }
98
+
99
+ export function buildHashString(hash, recurse, expanded = []) {
100
+ if (!hash) return "";
101
+
102
+ const buffer = [];
103
+ const hashes = [];
104
+ const expandableKeys = Preload.get("env_expandable_keys") || [];
105
+ _.each(hash, (v, k) => {
106
+ if (v === null) {
107
+ buffer.push("null");
108
+ } else if (Object.prototype.toString.call(v) === "[object Array]") {
109
+ let valueHtml = "";
110
+ if (
111
+ expandableKeys.indexOf(k) !== -1 &&
112
+ !recurse &&
113
+ expanded.indexOf(k) === -1
114
+ ) {
115
+ valueHtml = `${escapeHtml(
116
+ v[0]
117
+ )}, <a class="expand-list" data-key=${k}>${v.length - 1} more</a>`;
118
+ } else {
119
+ valueHtml = buildArrayString(v);
120
+ }
121
+ buffer.push(
122
+ "<tr><td>" + escapeHtml(k) + "</td><td>" + valueHtml + "</td></tr>"
123
+ );
124
+ } else if (typeof v === "object") {
125
+ hashes.push(k);
126
+ } else {
127
+ buffer.push(
128
+ "<tr><td>" + escapeHtml(k) + "</td><td>" + escapeHtml(v) + "</td></tr>"
129
+ );
130
+ }
131
+ });
132
+
133
+ if (_.size(hashes) > 0) {
134
+ _.each(hashes, function(k1) {
135
+ const v = hash[k1];
136
+ buffer.push("<tr><td></td><td><table>");
137
+ buffer.push(
138
+ "<td>" +
139
+ escapeHtml(k1) +
140
+ "</td><td>" +
141
+ buildHashString(v, true) +
142
+ "</td>"
143
+ );
144
+ buffer.push("</table></td></tr>");
145
+ });
146
+ }
147
+ const className = recurse ? "" : "env-table";
148
+ return "<table class='" + className + "'>" + buffer.join("\n") + "</table>";
149
+ }