Merge lp:~matsubara/launchpad/bug-436640 into lp:launchpad/db-devel

Proposed by Diogo Matsubara
Status: Merged
Approved by: Diogo Matsubara
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~matsubara/launchpad/bug-436640
Merge into: lp:launchpad/db-devel
Diff against target: 372 lines
6 files modified
lib/canonical/launchpad/webapp/errorlog.py (+17/-4)
lib/canonical/launchpad/webapp/publication.py (+1/-1)
lib/canonical/launchpad/webapp/tests/test_errorlog.py (+80/-32)
lib/canonical/launchpad/webapp/tests/test_publication.py (+3/-0)
lib/canonical/launchpad/webapp/tests/test_user_requested_oops.py (+10/-0)
lib/lp/bugs/doc/checkwatches.txt (+2/-0)
To merge this branch: bzr merge lp:~matsubara/launchpad/bug-436640
Reviewer Review Type Date Requested Status
Gary Poster (community) Approve
Review via email: mp+13016@code.launchpad.net

Commit message

[r=gary][ui=none][bug=436640] add api to generate informational oopses in errorlog.py and flag DisconnectionErrors and UserRequestOops as informational only.

To post a comment you must log in.
Revision history for this message
Diogo Matsubara (matsubara) wrote :

This branch add a new api: handling() to the ErrorReportingUtility which allows OOPS reports to be flagged as informational only. It also updates DisconnectionErrors and UserRequestOops to use said API.

Tests

$ bin/test -u test_errorlog
$ bin/test -u test_publication
$ bin/test -u test_user_requested_oops

Demo and Q/A

1. Open https://staging.launchpad.net/++oops++
2. Grab the OOPS id in the html source code
3. Wait 10 minutes for the oops to be synced from staging to devpad
4. https://lp-oops.canonical.com/oops/?oopsid=$oops_id_from_step_2
5. Check that the OOPS have the flag Informational: True in the left column

Revision history for this message
Gary Poster (gary) wrote :

Diogo, this is a great branch! I don't have any suggestions. Thank you!

Gary

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/webapp/errorlog.py'
--- lib/canonical/launchpad/webapp/errorlog.py 2009-08-06 14:25:28 +0000
+++ lib/canonical/launchpad/webapp/errorlog.py 2009-10-08 01:11:12 +0000
@@ -132,7 +132,7 @@
132 implements(IErrorReport)132 implements(IErrorReport)
133133
134 def __init__(self, id, type, value, time, pageid, tb_text, username,134 def __init__(self, id, type, value, time, pageid, tb_text, username,
135 url, duration, req_vars, db_statements):135 url, duration, req_vars, db_statements, informational):
136 self.id = id136 self.id = id
137 self.type = type137 self.type = type
138 self.value = value138 self.value = value
@@ -146,6 +146,7 @@
146 self.db_statements = db_statements146 self.db_statements = db_statements
147 self.branch_nick = versioninfo.branch_nick147 self.branch_nick = versioninfo.branch_nick
148 self.revno = versioninfo.revno148 self.revno = versioninfo.revno
149 self.informational = informational
149150
150 def __repr__(self):151 def __repr__(self):
151 return '<ErrorReport %s %s: %s>' % (self.id, self.type, self.value)152 return '<ErrorReport %s %s: %s>' % (self.id, self.type, self.value)
@@ -161,6 +162,7 @@
161 fp.write('User: %s\n' % _normalise_whitespace(self.username))162 fp.write('User: %s\n' % _normalise_whitespace(self.username))
162 fp.write('URL: %s\n' % _normalise_whitespace(self.url))163 fp.write('URL: %s\n' % _normalise_whitespace(self.url))
163 fp.write('Duration: %s\n' % self.duration)164 fp.write('Duration: %s\n' % self.duration)
165 fp.write('Informational: %s\n' % self.informational)
164 fp.write('\n')166 fp.write('\n')
165 safe_chars = ';/\\?:@&+$, ()*!'167 safe_chars = ';/\\?:@&+$, ()*!'
166 for key, value in self.req_vars:168 for key, value in self.req_vars:
@@ -184,6 +186,7 @@
184 username = msg.getheader('user')186 username = msg.getheader('user')
185 url = msg.getheader('url')187 url = msg.getheader('url')
186 duration = int(float(msg.getheader('duration', '-1')))188 duration = int(float(msg.getheader('duration', '-1')))
189 informational = msg.getheader('informational')
187190
188 # Explicitely use an iterator so we can process the file191 # Explicitely use an iterator so we can process the file
189 # sequentially. In most instances the iterator will actually192 # sequentially. In most instances the iterator will actually
@@ -217,7 +220,8 @@
217 tb_text = ''.join(lines)220 tb_text = ''.join(lines)
218221
219 return cls(id, exc_type, exc_value, date, pageid, tb_text,222 return cls(id, exc_type, exc_value, date, pageid, tb_text,
220 username, url, duration, req_vars, statements)223 username, url, duration, req_vars, statements,
224 informational)
221225
222226
223class ErrorReportingUtility:227class ErrorReportingUtility:
@@ -396,6 +400,10 @@
396400
397 def raising(self, info, request=None, now=None):401 def raising(self, info, request=None, now=None):
398 """See IErrorReportingUtility.raising()"""402 """See IErrorReportingUtility.raising()"""
403 self._raising(info, request=request, now=now, informational=False)
404
405 def _raising(self, info, request=None, now=None, informational=False):
406 """Private method used by raising() and handling()."""
399 if now is not None:407 if now is not None:
400 now = now.astimezone(UTC)408 now = now.astimezone(UTC)
401 else:409 else:
@@ -483,7 +491,8 @@
483491
484 entry = ErrorReport(oopsid, strtype, strv, now, pageid, tb_text,492 entry = ErrorReport(oopsid, strtype, strv, now, pageid, tb_text,
485 username, strurl, duration,493 username, strurl, duration,
486 req_vars, statements)494 req_vars, statements,
495 informational)
487 entry.write(open(filename, 'wb'))496 entry.write(open(filename, 'wb'))
488497
489 if request:498 if request:
@@ -495,6 +504,10 @@
495 finally:504 finally:
496 info = None505 info = None
497506
507 def handling(self, info, request=None, now=None):
508 """Flag ErrorReport as informational only."""
509 self._raising(info, request=request, now=now, informational=True)
510
498 def _do_copy_to_zlog(self, now, strtype, url, info, oopsid):511 def _do_copy_to_zlog(self, now, strtype, url, info, oopsid):
499 distant_past = datetime.datetime(1970, 1, 1, 0, 0, 0, tzinfo=UTC)512 distant_past = datetime.datetime(1970, 1, 1, 0, 0, 0, tzinfo=UTC)
500 when = _rate_restrict_pool.get(strtype, distant_past)513 when = _rate_restrict_pool.get(strtype, distant_past)
@@ -618,7 +631,7 @@
618 request.oopsid is not None or631 request.oopsid is not None or
619 not request.annotations.get(LAZR_OOPS_USER_REQUESTED_KEY, False)):632 not request.annotations.get(LAZR_OOPS_USER_REQUESTED_KEY, False)):
620 return None633 return None
621 globalErrorUtility.raising(634 globalErrorUtility.handling(
622 (UserRequestOops, UserRequestOops(), None), request)635 (UserRequestOops, UserRequestOops(), None), request)
623 return request.oopsid636 return request.oopsid
624637
625638
=== modified file 'lib/canonical/launchpad/webapp/publication.py'
--- lib/canonical/launchpad/webapp/publication.py 2009-10-01 15:36:20 +0000
+++ lib/canonical/launchpad/webapp/publication.py 2009-10-08 01:11:12 +0000
@@ -484,7 +484,7 @@
484 # Log a soft OOPS for DisconnectionErrors as per Bug #373837.484 # Log a soft OOPS for DisconnectionErrors as per Bug #373837.
485 # We need to do this before we re-raise the exception as a Retry.485 # We need to do this before we re-raise the exception as a Retry.
486 if isinstance(exc_info[1], DisconnectionError):486 if isinstance(exc_info[1], DisconnectionError):
487 getUtility(IErrorReportingUtility).raising(exc_info, request)487 getUtility(IErrorReportingUtility).handling(exc_info, request)
488488
489 def should_retry(exc_info):489 def should_retry(exc_info):
490 if not retry_allowed:490 if not retry_allowed:
491491
=== modified file 'lib/canonical/launchpad/webapp/tests/test_errorlog.py'
--- lib/canonical/launchpad/webapp/tests/test_errorlog.py 2009-07-29 14:28:18 +0000
+++ lib/canonical/launchpad/webapp/tests/test_errorlog.py 2009-10-08 01:11:12 +0000
@@ -58,7 +58,8 @@
58 [('name1', 'value1'), ('name2', 'value2'),58 [('name1', 'value1'), ('name2', 'value2'),
59 ('name1', 'value3')],59 ('name1', 'value3')],
60 [(1, 5, 'store_a', 'SELECT 1'),60 [(1, 5, 'store_a', 'SELECT 1'),
61 (5, 10, 'store_b', 'SELECT 2')])61 (5, 10, 'store_b', 'SELECT 2')],
62 False)
62 self.assertEqual(entry.id, 'id')63 self.assertEqual(entry.id, 'id')
63 self.assertEqual(entry.type, 'exc-type')64 self.assertEqual(entry.type, 'exc-type')
64 self.assertEqual(entry.value, 'exc-value')65 self.assertEqual(entry.value, 'exc-value')
@@ -69,6 +70,7 @@
69 self.assertEqual(entry.username, 'username')70 self.assertEqual(entry.username, 'username')
70 self.assertEqual(entry.url, 'url')71 self.assertEqual(entry.url, 'url')
71 self.assertEqual(entry.duration, 42)72 self.assertEqual(entry.duration, 42)
73 self.assertEqual(entry.informational, False)
72 self.assertEqual(len(entry.req_vars), 3)74 self.assertEqual(len(entry.req_vars), 3)
73 self.assertEqual(entry.req_vars[0], ('name1', 'value1'))75 self.assertEqual(entry.req_vars[0], ('name1', 'value1'))
74 self.assertEqual(entry.req_vars[1], ('name2', 'value2'))76 self.assertEqual(entry.req_vars[1], ('name2', 'value2'))
@@ -93,7 +95,7 @@
93 ('HTTP_REFERER', 'http://localhost:9000/'),95 ('HTTP_REFERER', 'http://localhost:9000/'),
94 ('name=foo', 'hello\nworld')],96 ('name=foo', 'hello\nworld')],
95 [(1, 5, 'store_a', 'SELECT 1'),97 [(1, 5, 'store_a', 'SELECT 1'),
96 (5, 10,'store_b', 'SELECT\n2')])98 (5, 10,'store_b', 'SELECT\n2')], False)
97 fp = StringIO.StringIO()99 fp = StringIO.StringIO()
98 entry.write(fp)100 entry.write(fp)
99 self.assertEqual(fp.getvalue(), dedent("""\101 self.assertEqual(fp.getvalue(), dedent("""\
@@ -107,6 +109,7 @@
107 User: Sample User109 User: Sample User
108 URL: http://localhost:9000/foo110 URL: http://localhost:9000/foo
109 Duration: 42111 Duration: 42
112 Informational: False
110113
111 HTTP_USER_AGENT=Mozilla/5.0114 HTTP_USER_AGENT=Mozilla/5.0
112 HTTP_REFERER=http://localhost:9000/115 HTTP_REFERER=http://localhost:9000/
@@ -377,19 +380,20 @@
377 self.assertEqual(lines[7], 'User: None\n')380 self.assertEqual(lines[7], 'User: None\n')
378 self.assertEqual(lines[8], 'URL: None\n')381 self.assertEqual(lines[8], 'URL: None\n')
379 self.assertEqual(lines[9], 'Duration: -1\n')382 self.assertEqual(lines[9], 'Duration: -1\n')
380 self.assertEqual(lines[10], '\n')383 self.assertEqual(lines[10], 'Informational: False\n')
384 self.assertEqual(lines[11], '\n')
381385
382 # no request vars386 # no request vars
383 self.assertEqual(lines[11], '\n')387 self.assertEqual(lines[12], '\n')
384388
385 # no database statements389 # no database statements
386 self.assertEqual(lines[12], '\n')390 self.assertEqual(lines[13], '\n')
387391
388 # traceback392 # traceback
389 self.assertEqual(lines[13], 'Traceback (most recent call last):\n')393 self.assertEqual(lines[14], 'Traceback (most recent call last):\n')
390 # Module canonical.launchpad.webapp.ftests.test_errorlog, ...394 # Module canonical.launchpad.webapp.ftests.test_errorlog, ...
391 # raise ArbitraryException(\'xyz\')395 # raise ArbitraryException(\'xyz\')
392 self.assertEqual(lines[16], 'ArbitraryException: xyz\n')396 self.assertEqual(lines[17], 'ArbitraryException: xyz\n')
393397
394 def test_raising_with_request(self):398 def test_raising_with_request(self):
395 """Test ErrorReportingUtility.raising() with a request"""399 """Test ErrorReportingUtility.raising() with a request"""
@@ -432,6 +436,7 @@
432 lines.pop(0), 'User: Login, 42, title, description |\\u25a0|\n')436 lines.pop(0), 'User: Login, 42, title, description |\\u25a0|\n')
433 self.assertEqual(lines.pop(0), 'URL: http://localhost:9000/foo\n')437 self.assertEqual(lines.pop(0), 'URL: http://localhost:9000/foo\n')
434 self.assertEqual(lines.pop(0), 'Duration: -1\n')438 self.assertEqual(lines.pop(0), 'Duration: -1\n')
439 self.assertEqual(lines.pop(0), 'Informational: False\n')
435 self.assertEqual(lines.pop(0), '\n')440 self.assertEqual(lines.pop(0), '\n')
436441
437 # request vars442 # request vars
@@ -479,7 +484,7 @@
479 errorfile = os.path.join(utility.errordir(now), '01800.T1')484 errorfile = os.path.join(utility.errordir(now), '01800.T1')
480 self.assertTrue(os.path.exists(errorfile))485 self.assertTrue(os.path.exists(errorfile))
481 lines = open(errorfile, 'r').readlines()486 lines = open(errorfile, 'r').readlines()
482 self.assertEqual(lines[15], 'xmlrpc args=(1, 2)\n')487 self.assertEqual(lines[16], 'xmlrpc args=(1, 2)\n')
483488
484 def test_raising_with_webservice_request(self):489 def test_raising_with_webservice_request(self):
485 # Test ErrorReportingUtility.raising() with a WebServiceRequest490 # Test ErrorReportingUtility.raising() with a WebServiceRequest
@@ -546,22 +551,23 @@
546 self.assertEqual(lines[7], 'User: None\n')551 self.assertEqual(lines[7], 'User: None\n')
547 self.assertEqual(lines[8], 'URL: https://launchpad.net/example\n')552 self.assertEqual(lines[8], 'URL: https://launchpad.net/example\n')
548 self.assertEqual(lines[9], 'Duration: -1\n')553 self.assertEqual(lines[9], 'Duration: -1\n')
549 self.assertEqual(lines[10], '\n')554 self.assertEqual(lines[10], 'Informational: False\n')
555 self.assertEqual(lines[11], '\n')
550556
551 # request vars557 # request vars
552 self.assertEqual(lines[11], 'name1=value1\n')558 self.assertEqual(lines[12], 'name1=value1\n')
553 self.assertEqual(lines[12], 'name1=value3\n')559 self.assertEqual(lines[13], 'name1=value3\n')
554 self.assertEqual(lines[13], 'name2=value2\n')560 self.assertEqual(lines[14], 'name2=value2\n')
555 self.assertEqual(lines[14], '\n')
556
557 # no database statements
558 self.assertEqual(lines[15], '\n')561 self.assertEqual(lines[15], '\n')
559562
563 # no database statements
564 self.assertEqual(lines[16], '\n')
565
560 # traceback566 # traceback
561 self.assertEqual(lines[16], 'Traceback (most recent call last):\n')567 self.assertEqual(lines[17], 'Traceback (most recent call last):\n')
562 # Module canonical.launchpad.webapp.ftests.test_errorlog, ...568 # Module canonical.launchpad.webapp.ftests.test_errorlog, ...
563 # raise ArbitraryException(\'xyz\')569 # raise ArbitraryException(\'xyz\')
564 self.assertEqual(lines[19], 'ArbitraryException: xyz\n')570 self.assertEqual(lines[20], 'ArbitraryException: xyz\n')
565571
566 # verify that the oopsid was set on the request572 # verify that the oopsid was set on the request
567 self.assertEqual(request.oopsid, 'OOPS-91T1')573 self.assertEqual(request.oopsid, 'OOPS-91T1')
@@ -598,20 +604,21 @@
598 self.assertEqual(lines[7], 'User: None\n')604 self.assertEqual(lines[7], 'User: None\n')
599 self.assertEqual(lines[8], 'URL: None\n')605 self.assertEqual(lines[8], 'URL: None\n')
600 self.assertEqual(lines[9], 'Duration: -1\n')606 self.assertEqual(lines[9], 'Duration: -1\n')
601 self.assertEqual(lines[10], '\n')607 self.assertEqual(lines[10], 'Informational: False\n')
608 self.assertEqual(lines[11], '\n')
602609
603 # no request vars610 # no request vars
604 self.assertEqual(lines[11], '\n')611 self.assertEqual(lines[12], '\n')
605612
606 # no database statements613 # no database statements
607 self.assertEqual(lines[12], '\n')614 self.assertEqual(lines[13], '\n')
608615
609 # traceback616 # traceback
610 self.assertEqual(lines[13], 'Traceback (most recent call last):\n')617 self.assertEqual(lines[14], 'Traceback (most recent call last):\n')
611 # Module canonical.launchpad.webapp.ftests.test_errorlog, ...618 # Module canonical.launchpad.webapp.ftests.test_errorlog, ...
612 # raise UnprintableException()619 # raise UnprintableException()
613 self.assertEqual(620 self.assertEqual(
614 lines[16], 'UnprintableException: <unprintable instance object>\n'621 lines[17], 'UnprintableException: <unprintable instance object>\n'
615 )622 )
616623
617 def test_raising_unauthorized_without_request(self):624 def test_raising_unauthorized_without_request(self):
@@ -715,16 +722,57 @@
715 self.assertEqual(lines[7], 'User: None\n')722 self.assertEqual(lines[7], 'User: None\n')
716 self.assertEqual(lines[8], 'URL: None\n')723 self.assertEqual(lines[8], 'URL: None\n')
717 self.assertEqual(lines[9], 'Duration: -1\n')724 self.assertEqual(lines[9], 'Duration: -1\n')
718 self.assertEqual(lines[10], '\n')725 self.assertEqual(lines[10], 'Informational: False\n')
719726 self.assertEqual(lines[11], '\n')
720 # no request vars727
721 self.assertEqual(lines[11], '\n')728 # no request vars
722729 self.assertEqual(lines[12], '\n')
723 # no database statements730
724 self.assertEqual(lines[12], '\n')731 # no database statements
725732 self.assertEqual(lines[13], '\n')
726 # traceback733
727 self.assertEqual(''.join(lines[13:17]), exc_tb)734 # traceback
735 self.assertEqual(''.join(lines[14:18]), exc_tb)
736
737 def test_handling(self):
738 """Test ErrorReportingUtility.handling()."""
739 utility = ErrorReportingUtility()
740 now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC)
741
742 try:
743 raise ArbitraryException('xyz')
744 except ArbitraryException:
745 utility.handling(sys.exc_info(), now=now)
746
747 errorfile = os.path.join(utility.errordir(now), '01800.T1')
748 self.assertTrue(os.path.exists(errorfile))
749 lines = open(errorfile, 'r').readlines()
750
751 # the header
752 self.assertEqual(lines[0], 'Oops-Id: OOPS-91T1\n')
753 self.assertEqual(lines[1], 'Exception-Type: ArbitraryException\n')
754 self.assertEqual(lines[2], 'Exception-Value: xyz\n')
755 self.assertEqual(lines[3], 'Date: 2006-04-01T00:30:00+00:00\n')
756 self.assertEqual(lines[4], 'Page-Id: \n')
757 self.assertEqual(lines[5], 'Branch: %s\n' % versioninfo.branch_nick)
758 self.assertEqual(lines[6], 'Revision: %s\n'% versioninfo.revno)
759 self.assertEqual(lines[7], 'User: None\n')
760 self.assertEqual(lines[8], 'URL: None\n')
761 self.assertEqual(lines[9], 'Duration: -1\n')
762 self.assertEqual(lines[10], 'Informational: True\n')
763 self.assertEqual(lines[11], '\n')
764
765 # no request vars
766 self.assertEqual(lines[12], '\n')
767
768 # no database statements
769 self.assertEqual(lines[13], '\n')
770
771 # traceback
772 self.assertEqual(lines[14], 'Traceback (most recent call last):\n')
773 # Module canonical.launchpad.webapp.ftests.test_errorlog, ...
774 # raise ArbitraryException(\'xyz\')
775 self.assertEqual(lines[17], 'ArbitraryException: xyz\n')
728776
729777
730class TestSensitiveRequestVariables(unittest.TestCase):778class TestSensitiveRequestVariables(unittest.TestCase):
731779
=== modified file 'lib/canonical/launchpad/webapp/tests/test_publication.py'
--- lib/canonical/launchpad/webapp/tests/test_publication.py 2009-09-17 14:29:22 +0000
+++ lib/canonical/launchpad/webapp/tests/test_publication.py 2009-10-08 01:11:12 +0000
@@ -115,6 +115,9 @@
115 # Ensure the OOPS mentions the correct exception115 # Ensure the OOPS mentions the correct exception
116 self.assertNotEqual(repr(next_oops).find("DisconnectionError"), -1)116 self.assertNotEqual(repr(next_oops).find("DisconnectionError"), -1)
117117
118 # Ensure the OOPS is correctly marked as informational only.
119 self.assertEqual(next_oops.informational, 'True')
120
118 # Ensure that it is different to the last logged OOPS.121 # Ensure that it is different to the last logged OOPS.
119 self.assertNotEqual(repr(last_oops), repr(next_oops))122 self.assertNotEqual(repr(last_oops), repr(next_oops))
120123
121124
=== modified file 'lib/canonical/launchpad/webapp/tests/test_user_requested_oops.py'
--- lib/canonical/launchpad/webapp/tests/test_user_requested_oops.py 2009-07-22 23:31:29 +0000
+++ lib/canonical/launchpad/webapp/tests/test_user_requested_oops.py 2009-10-08 01:11:12 +0000
@@ -6,6 +6,9 @@
66
7import unittest7import unittest
88
9from zope.component import getUtility
10from zope.error.interfaces import IErrorReportingUtility
11
9from lazr.restful.utils import get_current_browser_request12from lazr.restful.utils import get_current_browser_request
1013
11from canonical.launchpad.webapp.errorlog import (14from canonical.launchpad.webapp.errorlog import (
@@ -68,6 +71,13 @@
68 self.assertIs(context, result)71 self.assertIs(context, result)
69 self.assertTrue(request.annotations.get(LAZR_OOPS_USER_REQUESTED_KEY))72 self.assertTrue(request.annotations.get(LAZR_OOPS_USER_REQUESTED_KEY))
7073
74 def test_user_requested_oops_marked_informational(self):
75 # User requested oopses are flagged as informational only.
76 error_reporting_utility = getUtility(IErrorReportingUtility)
77 last_oops = error_reporting_utility.getLastOopsReport()
78 self.assertEqual(last_oops.type, 'UserRequestOops')
79 self.assertEqual(last_oops.informational, 'True')
80
7181
72def test_suite():82def test_suite():
73 return unittest.TestLoader().loadTestsFromName(__name__)83 return unittest.TestLoader().loadTestsFromName(__name__)
7484
=== modified file 'lib/lp/bugs/doc/checkwatches.txt'
--- lib/lp/bugs/doc/checkwatches.txt 2009-10-05 09:18:38 +0000
+++ lib/lp/bugs/doc/checkwatches.txt 2009-10-08 01:11:12 +0000
@@ -86,6 +86,7 @@
86 User: None86 User: None
87 URL: None87 URL: None
88 Duration: -188 Duration: -1
89 Informational: False
89 <BLANKLINE>90 <BLANKLINE>
90 error-explanation=ExternalBugtracker for ... is not known.91 error-explanation=ExternalBugtracker for ... is not known.
9192
@@ -157,6 +158,7 @@
157 User: None158 User: None
158 URL: http://bugs.example.com159 URL: http://bugs.example.com
159 Duration: -1160 Duration: -1
161 Informational: False
160 <BLANKLINE>162 <BLANKLINE>
161 baseurl=http://bugs.example.com163 baseurl=http://bugs.example.com
162 bugtracker=example-bugs164 bugtracker=example-bugs

Subscribers

People subscribed via source and target branches

to status/vote changes: