bullet 5.9.0 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +22 -1
  3. data/CHANGELOG.md +27 -0
  4. data/Gemfile.rails-4.0 +1 -1
  5. data/Gemfile.rails-4.1 +1 -1
  6. data/Gemfile.rails-4.2 +1 -1
  7. data/Gemfile.rails-5.0 +1 -1
  8. data/Gemfile.rails-5.1 +1 -1
  9. data/Gemfile.rails-5.2 +1 -1
  10. data/Gemfile.rails-6.0 +15 -0
  11. data/Gemfile.rails-6.1 +15 -0
  12. data/README.md +31 -10
  13. data/lib/bullet.rb +50 -26
  14. data/lib/bullet/active_job.rb +13 -0
  15. data/lib/bullet/active_record4.rb +9 -32
  16. data/lib/bullet/active_record41.rb +7 -27
  17. data/lib/bullet/active_record42.rb +8 -24
  18. data/lib/bullet/active_record5.rb +188 -179
  19. data/lib/bullet/active_record52.rb +176 -168
  20. data/lib/bullet/active_record60.rb +267 -0
  21. data/lib/bullet/active_record61.rb +267 -0
  22. data/lib/bullet/bullet_xhr.js +63 -0
  23. data/lib/bullet/dependency.rb +50 -36
  24. data/lib/bullet/detector/association.rb +26 -20
  25. data/lib/bullet/detector/base.rb +1 -2
  26. data/lib/bullet/detector/counter_cache.rb +13 -9
  27. data/lib/bullet/detector/n_plus_one_query.rb +22 -12
  28. data/lib/bullet/detector/unused_eager_loading.rb +6 -3
  29. data/lib/bullet/ext/object.rb +4 -2
  30. data/lib/bullet/mongoid4x.rb +2 -6
  31. data/lib/bullet/mongoid5x.rb +2 -6
  32. data/lib/bullet/mongoid6x.rb +2 -6
  33. data/lib/bullet/mongoid7x.rb +2 -6
  34. data/lib/bullet/notification/base.rb +14 -18
  35. data/lib/bullet/notification/n_plus_one_query.rb +2 -4
  36. data/lib/bullet/notification/unused_eager_loading.rb +2 -4
  37. data/lib/bullet/rack.rb +50 -25
  38. data/lib/bullet/stack_trace_filter.rb +6 -12
  39. data/lib/bullet/version.rb +1 -1
  40. data/lib/generators/bullet/install_generator.rb +23 -23
  41. data/perf/benchmark.rb +8 -14
  42. data/spec/bullet/detector/counter_cache_spec.rb +6 -6
  43. data/spec/bullet/detector/n_plus_one_query_spec.rb +7 -3
  44. data/spec/bullet/detector/unused_eager_loading_spec.rb +19 -6
  45. data/spec/bullet/ext/object_spec.rb +9 -4
  46. data/spec/bullet/notification/base_spec.rb +1 -3
  47. data/spec/bullet/notification/n_plus_one_query_spec.rb +16 -3
  48. data/spec/bullet/notification/unused_eager_loading_spec.rb +5 -1
  49. data/spec/bullet/rack_spec.rb +86 -6
  50. data/spec/bullet/registry/association_spec.rb +2 -2
  51. data/spec/bullet/registry/base_spec.rb +1 -1
  52. data/spec/bullet_spec.rb +11 -30
  53. data/spec/integration/active_record/association_spec.rb +44 -136
  54. data/spec/integration/counter_cache_spec.rb +11 -31
  55. data/spec/integration/mongoid/association_spec.rb +18 -32
  56. data/spec/models/folder.rb +1 -2
  57. data/spec/models/group.rb +1 -2
  58. data/spec/models/page.rb +1 -2
  59. data/spec/models/writer.rb +1 -2
  60. data/spec/spec_helper.rb +6 -10
  61. data/spec/support/bullet_ext.rb +8 -9
  62. data/spec/support/mongo_seed.rb +2 -16
  63. data/test.sh +1 -0
  64. metadata +12 -7
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Bullet
4
4
  module Detector
5
- class Base
6
- end
5
+ class Base; end
7
6
  end
8
7
  end
@@ -3,16 +3,17 @@
3
3
  module Bullet
4
4
  module Detector
5
5
  class CounterCache < Base
6
- class <<self
6
+ class << self
7
7
  def add_counter_cache(object, associations)
8
8
  return unless Bullet.start?
9
9
  return unless Bullet.counter_cache_enable?
10
- return unless object.primary_key_value
10
+ return unless object.bullet_primary_key_value
11
11
 
12
- Bullet.debug('Detector::CounterCache#add_counter_cache', "object: #{object.bullet_key}, associations: #{associations}")
13
- if conditions_met?(object, associations)
14
- create_notification object.class.to_s, associations
15
- end
12
+ Bullet.debug(
13
+ 'Detector::CounterCache#add_counter_cache',
14
+ "object: #{object.bullet_key}, associations: #{associations}"
15
+ )
16
+ create_notification object.class.to_s, associations if conditions_met?(object, associations)
16
17
  end
17
18
 
18
19
  def add_possible_objects(object_or_objects)
@@ -20,16 +21,19 @@ module Bullet
20
21
  return unless Bullet.counter_cache_enable?
21
22
 
22
23
  objects = Array(object_or_objects)
23
- return if objects.map(&:primary_key_value).compact.empty?
24
+ return if objects.map(&:bullet_primary_key_value).compact.empty?
24
25
 
25
- Bullet.debug('Detector::CounterCache#add_possible_objects', "objects: #{objects.map(&:bullet_key).join(', ')}")
26
+ Bullet.debug(
27
+ 'Detector::CounterCache#add_possible_objects',
28
+ "objects: #{objects.map(&:bullet_key).join(', ')}"
29
+ )
26
30
  objects.each { |object| possible_objects.add object.bullet_key }
27
31
  end
28
32
 
29
33
  def add_impossible_object(object)
30
34
  return unless Bullet.start?
31
35
  return unless Bullet.counter_cache_enable?
32
- return unless object.primary_key_value
36
+ return unless object.bullet_primary_key_value
33
37
 
34
38
  Bullet.debug('Detector::CounterCache#add_impossible_object', "object: #{object.bullet_key}")
35
39
  impossible_objects.add object.bullet_key
@@ -6,7 +6,7 @@ module Bullet
6
6
  extend Dependency
7
7
  extend StackTraceFilter
8
8
 
9
- class <<self
9
+ class << self
10
10
  # executed when object.assocations is called.
11
11
  # first, it keeps this method call for object.association.
12
12
  # then, it checks if this associations call is unpreload.
@@ -14,12 +14,15 @@ module Bullet
14
14
  def call_association(object, associations)
15
15
  return unless Bullet.start?
16
16
  return unless Bullet.n_plus_one_query_enable?
17
- return unless object.primary_key_value
17
+ return unless object.bullet_primary_key_value
18
18
  return if inversed_objects.include?(object.bullet_key, associations)
19
19
 
20
20
  add_call_object_associations(object, associations)
21
21
 
22
- Bullet.debug('Detector::NPlusOneQuery#call_association', "object: #{object.bullet_key}, associations: #{associations}")
22
+ Bullet.debug(
23
+ 'Detector::NPlusOneQuery#call_association',
24
+ "object: #{object.bullet_key}, associations: #{associations}"
25
+ )
23
26
  if !excluded_stacktrace_path? && conditions_met?(object, associations)
24
27
  Bullet.debug('detect n + 1 query', "object: #{object.bullet_key}, associations: #{associations}")
25
28
  create_notification caller_in_project, object.class.to_s, associations
@@ -31,16 +34,19 @@ module Bullet
31
34
  return unless Bullet.n_plus_one_query_enable?
32
35
 
33
36
  objects = Array(object_or_objects)
34
- return if objects.map(&:primary_key_value).compact.empty?
37
+ return if objects.map(&:bullet_primary_key_value).compact.empty?
35
38
 
36
- Bullet.debug('Detector::NPlusOneQuery#add_possible_objects', "objects: #{objects.map(&:bullet_key).join(', ')}")
39
+ Bullet.debug(
40
+ 'Detector::NPlusOneQuery#add_possible_objects',
41
+ "objects: #{objects.map(&:bullet_key).join(', ')}"
42
+ )
37
43
  objects.each { |object| possible_objects.add object.bullet_key }
38
44
  end
39
45
 
40
46
  def add_impossible_object(object)
41
47
  return unless Bullet.start?
42
48
  return unless Bullet.n_plus_one_query_enable?
43
- return unless object.primary_key_value
49
+ return unless object.bullet_primary_key_value
44
50
 
45
51
  Bullet.debug('Detector::NPlusOneQuery#add_impossible_object', "object: #{object.bullet_key}")
46
52
  impossible_objects.add object.bullet_key
@@ -49,9 +55,12 @@ module Bullet
49
55
  def add_inversed_object(object, association)
50
56
  return unless Bullet.start?
51
57
  return unless Bullet.n_plus_one_query_enable?
52
- return unless object.primary_key_value
58
+ return unless object.bullet_primary_key_value
53
59
 
54
- Bullet.debug('Detector::NPlusOneQuery#add_inversed_object', "object: #{object.bullet_key}, association: #{association}")
60
+ Bullet.debug(
61
+ 'Detector::NPlusOneQuery#add_inversed_object',
62
+ "object: #{object.bullet_key}, association: #{association}"
63
+ )
55
64
  inversed_objects.add object.bullet_key, association
56
65
  end
57
66
 
@@ -72,10 +81,11 @@ module Bullet
72
81
  def association?(object, associations)
73
82
  value = object_associations[object.bullet_key]
74
83
  value&.each do |v|
75
- # associations == v comparison order is important here because
76
- # v variable might be a squeel node where :== method is redefined,
77
- # so it does not compare values at all and return unexpected results
78
- result = v.is_a?(Hash) ? v.key?(associations) : associations == v
84
+ # associations == v comparison order is important here because
85
+ # v variable might be a squeel node where :== method is redefined,
86
+ # so it does not compare values at all and return unexpected results
87
+ result =
88
+ v.is_a?(Hash) ? v.key?(associations) : associations == v
79
89
  return true if result
80
90
  end
81
91
 
@@ -6,7 +6,7 @@ module Bullet
6
6
  extend Dependency
7
7
  extend StackTraceFilter
8
8
 
9
- class <<self
9
+ class << self
10
10
  # check if there are unused preload associations.
11
11
  # get related_objects from eager_loadings associated with object and associations
12
12
  # get call_object_association from associations of call_object_associations whose object is in related_objects
@@ -27,9 +27,12 @@ module Bullet
27
27
  def add_eager_loadings(objects, associations)
28
28
  return unless Bullet.start?
29
29
  return unless Bullet.unused_eager_loading_enable?
30
- return if objects.map(&:primary_key_value).compact.empty?
30
+ return if objects.map(&:bullet_primary_key_value).compact.empty?
31
31
 
32
- Bullet.debug('Detector::UnusedEagerLoading#add_eager_loadings', "objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}")
32
+ Bullet.debug(
33
+ 'Detector::UnusedEagerLoading#add_eager_loadings',
34
+ "objects: #{objects.map(&:bullet_key).join(', ')}, associations: #{associations}"
35
+ )
33
36
  bullet_keys = objects.map(&:bullet_key)
34
37
 
35
38
  to_add = []
@@ -2,10 +2,12 @@
2
2
 
3
3
  class Object
4
4
  def bullet_key
5
- "#{self.class}:#{primary_key_value}"
5
+ "#{self.class}:#{bullet_primary_key_value}"
6
6
  end
7
7
 
8
- def primary_key_value
8
+ def bullet_primary_key_value
9
+ return if respond_to?(:persisted?) && !persisted?
10
+
9
11
  if self.class.respond_to?(:primary_keys) && self.class.primary_keys
10
12
  self.class.primary_keys.map { |primary_key| send primary_key }.join(',')
11
13
  elsif self.class.respond_to?(:primary_key) && self.class.primary_key
@@ -37,9 +37,7 @@ module Bullet
37
37
 
38
38
  def eager_load(docs)
39
39
  associations = criteria.inclusions.map(&:name)
40
- docs.each do |doc|
41
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
42
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
43
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
44
42
  origin_eager_load(docs)
45
43
  end
@@ -50,9 +48,7 @@ module Bullet
50
48
 
51
49
  def get_relation(name, metadata, object, reload = false)
52
50
  result = origin_get_relation(name, metadata, object, reload)
53
- if metadata.macro !~ /embed/
54
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
55
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
56
52
  result
57
53
  end
58
54
  end
@@ -37,9 +37,7 @@ module Bullet
37
37
 
38
38
  def eager_load(docs)
39
39
  associations = criteria.inclusions.map(&:name)
40
- docs.each do |doc|
41
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
42
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
43
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
44
42
  origin_eager_load(docs)
45
43
  end
@@ -50,9 +48,7 @@ module Bullet
50
48
 
51
49
  def get_relation(name, metadata, object, reload = false)
52
50
  result = origin_get_relation(name, metadata, object, reload)
53
- if metadata.macro !~ /embed/
54
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
55
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
56
52
  result
57
53
  end
58
54
  end
@@ -37,9 +37,7 @@ module Bullet
37
37
 
38
38
  def eager_load(docs)
39
39
  associations = criteria.inclusions.map(&:name)
40
- docs.each do |doc|
41
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
42
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
43
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
44
42
  origin_eager_load(docs)
45
43
  end
@@ -50,9 +48,7 @@ module Bullet
50
48
 
51
49
  def get_relation(name, metadata, object, reload = false)
52
50
  result = origin_get_relation(name, metadata, object, reload)
53
- if metadata.macro !~ /embed/
54
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
55
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) if metadata.macro !~ /embed/
56
52
  result
57
53
  end
58
54
  end
@@ -37,9 +37,7 @@ module Bullet
37
37
 
38
38
  def eager_load(docs)
39
39
  associations = criteria.inclusions.map(&:name)
40
- docs.each do |doc|
41
- Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations)
42
- end
40
+ docs.each { |doc| Bullet::Detector::NPlusOneQuery.add_object_associations(doc, associations) }
43
41
  Bullet::Detector::UnusedEagerLoading.add_eager_loadings(docs, associations)
44
42
  origin_eager_load(docs)
45
43
  end
@@ -50,9 +48,7 @@ module Bullet
50
48
 
51
49
  def get_relation(name, association, object, reload = false)
52
50
  result = origin_get_relation(name, association, object, reload)
53
- unless association.embedded?
54
- Bullet::Detector::NPlusOneQuery.call_association(self, name)
55
- end
51
+ Bullet::Detector::NPlusOneQuery.call_association(self, name) unless association.embedded?
56
52
  result
57
53
  end
58
54
  end
@@ -8,7 +8,8 @@ module Bullet
8
8
 
9
9
  def initialize(base_class, association_or_associations, path = nil)
10
10
  @base_class = base_class
11
- @associations = association_or_associations.is_a?(Array) ? association_or_associations : [association_or_associations]
11
+ @associations =
12
+ association_or_associations.is_a?(Array) ? association_or_associations : [association_or_associations]
12
13
  @path = path
13
14
  end
14
15
 
@@ -25,16 +26,16 @@ module Bullet
25
26
  end
26
27
 
27
28
  def whoami
28
- @user ||= ENV['USER'].presence || (begin
29
- `whoami`.chomp
30
- rescue StandardError
31
- ''
32
- end)
33
- if @user.present?
34
- "user: #{@user}"
35
- else
36
- ''
37
- end
29
+ @user ||=
30
+ ENV['USER'].presence ||
31
+ (
32
+ begin
33
+ `whoami`.chomp
34
+ rescue StandardError
35
+ ''
36
+ end
37
+ )
38
+ @user.present? ? "user: #{@user}" : ''
38
39
  end
39
40
 
40
41
  def body_with_caller
@@ -54,12 +55,7 @@ module Bullet
54
55
  end
55
56
 
56
57
  def notification_data
57
- {
58
- user: whoami,
59
- url: url,
60
- title: title,
61
- body: body_with_caller
62
- }
58
+ { user: whoami, url: url, title: title, body: body_with_caller }
63
59
  end
64
60
 
65
61
  def eql?(other)
@@ -77,7 +73,7 @@ module Bullet
77
73
  end
78
74
 
79
75
  def associations_str
80
- ":includes => #{@associations.map { |a| a.to_s.to_sym unless a.is_a? Hash }.inspect}"
76
+ ".includes(#{@associations.map { |a| a.to_s.to_sym }.inspect})"
81
77
  end
82
78
  end
83
79
  end
@@ -10,7 +10,7 @@ module Bullet
10
10
  end
11
11
 
12
12
  def body
13
- "#{klazz_associations_str}\n Add to your finder: #{associations_str}"
13
+ "#{klazz_associations_str}\n Add to your query: #{associations_str}"
14
14
  end
15
15
 
16
16
  def title
@@ -18,9 +18,7 @@ module Bullet
18
18
  end
19
19
 
20
20
  def notification_data
21
- super.merge(
22
- backtrace: []
23
- )
21
+ super.merge(backtrace: [])
24
22
  end
25
23
 
26
24
  protected
@@ -10,7 +10,7 @@ module Bullet
10
10
  end
11
11
 
12
12
  def body
13
- "#{klazz_associations_str}\n Remove from your finder: #{associations_str}"
13
+ "#{klazz_associations_str}\n Remove from your query: #{associations_str}"
14
14
  end
15
15
 
16
16
  def title
@@ -18,9 +18,7 @@ module Bullet
18
18
  end
19
19
 
20
20
  def notification_data
21
- super.merge(
22
- backtrace: []
23
- )
21
+ super.merge(backtrace: [])
24
22
  end
25
23
 
26
24
  protected
@@ -15,13 +15,19 @@ module Bullet
15
15
  status, headers, response = @app.call(env)
16
16
 
17
17
  response_body = nil
18
+
18
19
  if Bullet.notification?
19
- if !file?(headers) && !sse?(headers) && !empty?(response) &&
20
- status == 200 && html_request?(headers, response)
21
- response_body = response_body(response)
22
- response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
23
- response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
24
- headers['Content-Length'] = response_body.bytesize.to_s
20
+ if Bullet.inject_into_page? && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
21
+ if html_request?(headers, response)
22
+ response_body = response_body(response)
23
+ response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
24
+ response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
25
+ response_body = append_to_html_body(response_body, xhr_script)
26
+ headers['Content-Length'] = response_body.bytesize.to_s
27
+ else
28
+ set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer
29
+ set_header(headers, 'X-bullet-console-text', Bullet.text_notifications) if Bullet.console_enabled?
30
+ end
25
31
  end
26
32
  Bullet.perform_out_of_channel_notifications(env)
27
33
  end
@@ -32,20 +38,15 @@ module Bullet
32
38
 
33
39
  # fix issue if response's body is a Proc
34
40
  def empty?(response)
35
- # response may be ["Not Found"], ["Move Permanently"], etc.
36
- if rails?
37
- (response.is_a?(Array) && response.size <= 1) ||
38
- !response.respond_to?(:body) ||
39
- !response_body(response).respond_to?(:empty?) ||
40
- response_body(response).empty?
41
- else
42
- body = response_body(response)
43
- body.nil? || body.empty?
44
- end
41
+ # response may be ["Not Found"], ["Move Permanently"], etc, but
42
+ # those should not happen if the status is 200
43
+ body = response_body(response)
44
+ body.nil? || body.empty?
45
45
  end
46
46
 
47
47
  def append_to_html_body(response_body, content)
48
48
  body = response_body.dup
49
+ content = content.html_safe if content.respond_to?(:html_safe)
49
50
  if body.include?('</body>')
50
51
  position = body.rindex('</body>')
51
52
  body.insert(position, content)
@@ -55,7 +56,15 @@ module Bullet
55
56
  end
56
57
 
57
58
  def footer_note
58
- "<div #{footer_div_attributes}>" + footer_close_button + Bullet.footer_info.uniq.join('<br>') + '</div>'
59
+ "<details #{details_attributes}><summary #{summary_attributes}>Bullet Warnings</summary><div #{footer_content_attributes}>#{Bullet.footer_info.uniq.join('<br>')}#{footer_console_message}</div></details>"
60
+ end
61
+
62
+ def set_header(headers, header_name, header_array)
63
+ # Many proxy applications such as Nginx and AWS ELB limit
64
+ # the size a header to 8KB, so truncate the list of reports to
65
+ # be under that limit
66
+ header_array.pop while header_array.to_json.length > 8 * 1024
67
+ headers[header_name] = header_array.to_json
59
68
  end
60
69
 
61
70
  def file?(headers)
@@ -80,18 +89,34 @@ module Bullet
80
89
 
81
90
  private
82
91
 
83
- def footer_div_attributes
92
+ def details_attributes
93
+ <<~EOF
94
+ id="bullet-footer" data-is-bullet-footer
95
+ style="cursor: pointer; position: fixed; left: 0px; bottom: 0px; z-index: 9999; background: #fdf2f2; color: #9b1c1c; font-size: 12px; border-radius: 0px 8px 0px 0px; border: 1px solid #9b1c1c;"
96
+ EOF
97
+ end
98
+
99
+ def summary_attributes
100
+ <<~EOF
101
+ style="font-weight: 600; padding: 2px 8px"
102
+ EOF
103
+ end
104
+
105
+ def footer_content_attributes
84
106
  <<~EOF
85
- data-is-bullet-footer ondblclick="this.parentNode.removeChild(this);" style="position: fixed; bottom: 0pt; left: 0pt; cursor: pointer; border-style: solid; border-color: rgb(153, 153, 153);
86
- -moz-border-top-colors: none; -moz-border-right-colors: none; -moz-border-bottom-colors: none;
87
- -moz-border-left-colors: none; -moz-border-image: none; border-width: 2pt 2pt 0px 0px;
88
- padding: 3px 5px; border-radius: 0pt 10pt 0pt 0px; background: none repeat scroll 0% 0% rgba(200, 200, 200, 0.8);
89
- color: rgb(119, 119, 119); font-size: 16px; font-family: 'Arial', sans-serif; z-index:9999;"
107
+ style="padding: 8px; border-top: 1px solid #9b1c1c;"
90
108
  EOF
91
109
  end
92
110
 
93
- def footer_close_button
94
- "<span onclick='this.parentNode.remove()' style='position:absolute; right: 10px; top: 0px; font-weight: bold; color: #333;'>&times;</span>"
111
+ def footer_console_message
112
+ if Bullet.console_enabled?
113
+ "<br/><span style='font-style: italic;'>See 'Uniform Notifier' in JS Console for Stacktrace</span>"
114
+ end
115
+ end
116
+
117
+ # Make footer work for XHR requests by appending data to the footer
118
+ def xhr_script
119
+ "<script type='text/javascript'>#{File.read("#{__dir__}/bullet_xhr.js")}</script>"
95
120
  end
96
121
  end
97
122
  end